HR project¶

Table of Contents

  • 1  Описание проекта
  • 2  Задача 1: Предсказание уровня удовлетворённости сотрудника
    • 2.1  Описание данных
    • 2.2  Загрузка данных
      • 2.2.1  Изучение загруженных датасетов
      • 2.2.2  Определение наличия и количества пропусков в датасетах
    • 2.3  Предобработка данных
      • 2.3.1  Проверка датафреймов на явные дубликаты
      • 2.3.2  Проверка датафреймов на неявные дубликаты
    • 2.4  Исследовательский анализ данных
      • 2.4.1  Функции для исследовательского анализа данных
      • 2.4.2  Объединение тестовой выборки для статистического анализа
      • 2.4.3  Анализ количественных и качественных признаков
        • 2.4.3.1  Для тренировочных данных train_job_satisfaction_rate
        • 2.4.3.2  Для тестовых данных test_full
      • 2.4.4  Корреляционный анализ
    • 2.5  Подготовка данных
    • 2.6  Обучение модели
    • 2.7  Оформление выводов
  • 3  Задача 2: предсказание увольнения сотрудника из компании
    • 3.1  Загрузка данных
      • 3.1.1  Изучение загруженных датасетов
      • 3.1.2  Определение наличия и количества пропусков
    • 3.2  Предобработка данных
      • 3.2.1  Проверка датафреймов на явные дубликаты
      • 3.2.2  Проверка датафреймов на неявные дубликаты
    • 3.3  Исследовательский анализ данных
      • 3.3.1  Объединение тестовой выборки для статистического анализа
      • 3.3.2  Анализ количественных и качественных признаков
        • 3.3.2.1  Для тренировочных данных train_quit
        • 3.3.2.2  Иследование категориальных данных датафрейма train_quit в зависимости от значения целевого признака quit
      • 3.3.3  Корреляционный анализ
        • 3.3.3.1  Для тестовых данных test_quit_full
        • 3.3.3.2  Иследование категориальных данных датафрейма test_quit_full в зависимости от значения целевого признака quit
      • 3.3.4  Корреляционный анализ
      • 3.3.5  Портрет уходящего работника
      • 3.3.6  Визуализация и сравнение распределения признака job_satisfaction_rate для ушедших и оставшихся сотрудников
    • 3.4  Добавление нового входного признака
    • 3.5  Подготовка данных
    • 3.6  Обучение модели
    • 3.7  Оформление выводов
  • 4  Общий вывод:

Описание проекта¶

HR-аналитики компании «Работа с заботой» помогают бизнесу оптимизировать управление персоналом: бизнес предоставляет данные, а аналитики предлагают, как избежать финансовых потерь и оттока сотрудников. В этом HR-аналитикам пригодится машинное обучение, с помощью которого получится быстрее и точнее отвечать на вопросы бизнеса.

Компания предоставила данные с характеристиками сотрудников компании. Среди них — уровень удовлетворённости сотрудника работой в компании. Эту информацию получили из форм обратной связи: сотрудники заполняют тест-опросник, и по его результатам рассчитывается доля их удовлетворённости от 0 до 1, где 0 — совершенно неудовлетворён, 1 — полностью удовлетворён.
Собирать данные такими опросниками не так легко: компания большая, и всех сотрудников надо сначала оповестить об опросе, а затем проследить, что все его прошли.

У нас будет несколько задач:
Первая задача — построить модель, которая сможет предсказать уровень удовлетворённости сотрудника на основе данных заказчика.
Почему бизнесу это важно: удовлетворённость работой напрямую влияет на отток сотрудников. А предсказание оттока — одна из важнейших задач HR-аналитиков. Внезапные увольнения несут в себе риски для компании, особенно если уходит важный сотрудник.
Вторая задача — построить модель, которая сможет на основе данных заказчика предсказать то, что сотрудник уволится из компании.

Задача 1: Предсказание уровня удовлетворённости сотрудника¶

Описание данных¶

Для этой задачи заказчик предоставил данные с признаками:

  • id — уникальный идентификатор сотрудника
  • dept — отдел, в котором работает сотрудник
  • level — уровень занимаемой должности
  • workload — уровень загруженности сотрудника
  • employment_years — длительность работы в компании (в годах)
  • last_year_promo — показывает, было ли повышение за последний год
  • last_year_violations — показывает, нарушал ли сотрудник трудовой договор за последний год
  • supervisor_evaluation — оценка качества работы сотрудника, которую дал руководитель
  • salary — ежемесячная зарплата сотрудника
  • job_satisfaction_rate — уровень удовлетворённости сотрудника работой в компании, целевой признак

Имортирование необходимых для анализа библиотек

In [1]:
%%capture
!pip install -U scikit-learn
In [2]:
!pip -q install phik
!pip -q install shap
!pip -q install pandas
In [3]:
import os
import pandas as pd
import matplotlib.pyplot as plt
import math
import numpy as np
import re
import seaborn as sns
import shap

import warnings
warnings.simplefilter(action='ignore', category=FutureWarning)

from scipy.stats import shapiro

from phik import phik_matrix

from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import (
    StandardScaler, OneHotEncoder,
    MinMaxScaler, OrdinalEncoder, LabelEncoder
)
from sklearn.impute import SimpleImputer
from sklearn.model_selection import GridSearchCV
from sklearn.dummy import DummyRegressor, DummyClassifier
from sklearn.linear_model import LinearRegression, LogisticRegression
from sklearn.tree import DecisionTreeRegressor, DecisionTreeClassifier
from sklearn.svm import SVR, SVC
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import roc_auc_score, make_scorer
In [4]:
# Настройки вывода графиков
plt.rcParams["axes.titlesize"] = 16  # Размер шрифта
plt.rcParams["axes.titleweight"] = "bold"  # Толщина шрифта
In [5]:
# Константы
RANDOM_STATE = 42
try:
    TERM_SIZE = os.get_terminal_size()
except OSError:
    TERM_SIZE = os.terminal_size((80, 24))  # Размер по умолчанию

Загрузка данных¶

In [6]:
try:
    train_job_satisfaction_rate = pd.read_csv("C:\\Data-science\\ds_csv\\train_job_satisfaction_rate.csv")
    test_features = pd.read_csv("C:\\Data-science\\ds_csv\\test_features.csv")
    test_target_job_satisfaction_rate = pd.read_csv("C:\\Data-science\\ds_csv\\test_target_job_satisfaction_rate.csv")
except:
    try:
        train_job_satisfaction_rate = pd.read_csv('/datasets/train_job_satisfaction_rate.csv')
        test_features = pd.read_csv('/datasets/test_features.csv')
        test_target_job_satisfaction_rate = pd.read_csv('/datasets/test_target_job_satisfaction_rate.csv')
    except:
        raise FileNotFoundError  

Изучение загруженных датасетов¶

In [7]:
# Создаем словарь, чтобы перебрать все импортируемые датафреймы
dataframes = {
    "train_job_satisfaction_rate": train_job_satisfaction_rate,
    "test_features": test_features,
    "test_target_job_satisfaction_rate": test_target_job_satisfaction_rate
}


# Цикл по каждому DataFrame с выводом имени, информации и первых строк
for name, data in dataframes.items():
    print(f'\033[1mНаименование анализируемого датафрейма:\033[0m {name}')
    print()
    data.info()  # Выводим информацию о DataFrame
    display(data.head(5))  # Отображаем первые 5 строк
    print('=' * TERM_SIZE.columns)  # Линия-разделитель по ширине терминала
Наименование анализируемого датафрейма: train_job_satisfaction_rate

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 4000 entries, 0 to 3999
Data columns (total 10 columns):
 #   Column                 Non-Null Count  Dtype  
---  ------                 --------------  -----  
 0   id                     4000 non-null   int64  
 1   dept                   3994 non-null   object 
 2   level                  3996 non-null   object 
 3   workload               4000 non-null   object 
 4   employment_years       4000 non-null   int64  
 5   last_year_promo        4000 non-null   object 
 6   last_year_violations   4000 non-null   object 
 7   supervisor_evaluation  4000 non-null   int64  
 8   salary                 4000 non-null   int64  
 9   job_satisfaction_rate  4000 non-null   float64
dtypes: float64(1), int64(4), object(5)
memory usage: 312.6+ KB
id dept level workload employment_years last_year_promo last_year_violations supervisor_evaluation salary job_satisfaction_rate
0 155278 sales junior medium 2 no no 1 24000 0.58
1 653870 hr junior high 2 no no 5 38400 0.76
2 184592 sales junior low 1 no no 2 12000 0.11
3 171431 technology junior low 4 no no 2 18000 0.37
4 693419 hr junior medium 1 no no 3 22800 0.20
===============================================================================================================
Наименование анализируемого датафрейма: test_features

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 2000 entries, 0 to 1999
Data columns (total 9 columns):
 #   Column                 Non-Null Count  Dtype 
---  ------                 --------------  ----- 
 0   id                     2000 non-null   int64 
 1   dept                   1998 non-null   object
 2   level                  1999 non-null   object
 3   workload               2000 non-null   object
 4   employment_years       2000 non-null   int64 
 5   last_year_promo        2000 non-null   object
 6   last_year_violations   2000 non-null   object
 7   supervisor_evaluation  2000 non-null   int64 
 8   salary                 2000 non-null   int64 
dtypes: int64(4), object(5)
memory usage: 140.8+ KB
id dept level workload employment_years last_year_promo last_year_violations supervisor_evaluation salary
0 485046 marketing junior medium 2 no no 5 28800
1 686555 hr junior medium 1 no no 4 30000
2 467458 sales middle low 5 no no 4 19200
3 418655 sales middle low 6 no no 4 19200
4 789145 hr middle medium 5 no no 5 40800
===============================================================================================================
Наименование анализируемого датафрейма: test_target_job_satisfaction_rate

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 2000 entries, 0 to 1999
Data columns (total 2 columns):
 #   Column                 Non-Null Count  Dtype  
---  ------                 --------------  -----  
 0   id                     2000 non-null   int64  
 1   job_satisfaction_rate  2000 non-null   float64
dtypes: float64(1), int64(1)
memory usage: 31.4 KB
id job_satisfaction_rate
0 130604 0.74
1 825977 0.75
2 418490 0.60
3 555320 0.72
4 826430 0.08
===============================================================================================================

Определение наличия и количества пропусков в датасетах¶

In [8]:
# Цикл по каждому DataFrame с выводом имени, информации и первых строк
for name, data in dataframes.items():
    print(f'\033[1mНаименование анализируемого датафрейма:\033[0m {name}')
    display(data.isna().sum())  # Отображаем первые 5 строк
    print('=' * TERM_SIZE.columns)  # Линия-разделитель по ширине терминала
Наименование анализируемого датафрейма: train_job_satisfaction_rate
id                       0
dept                     6
level                    4
workload                 0
employment_years         0
last_year_promo          0
last_year_violations     0
supervisor_evaluation    0
salary                   0
job_satisfaction_rate    0
dtype: int64
===============================================================================================================
Наименование анализируемого датафрейма: test_features
id                       0
dept                     2
level                    1
workload                 0
employment_years         0
last_year_promo          0
last_year_violations     0
supervisor_evaluation    0
salary                   0
dtype: int64
===============================================================================================================
Наименование анализируемого датафрейма: test_target_job_satisfaction_rate
id                       0
job_satisfaction_rate    0
dtype: int64
===============================================================================================================

Вывод по предварительному анализу:

В ходе предварительного анализа данных было выявлено:

  • В датафреймах присутствуют пропуски;
  • Название столбцов имеют форму snake_case.

Предобработка данных¶

Проверка датафреймов на явные дубликаты¶

In [9]:
for name, data in dataframes.items():
    print(f"Явных дубликатов в датасете {name} - {data.duplicated().sum()}: "
          if data.duplicated().sum() > 0 else f"Явных дубликатов в датасете {name} - НЕТ")
Явных дубликатов в датасете train_job_satisfaction_rate - НЕТ
Явных дубликатов в датасете test_features - НЕТ
Явных дубликатов в датасете test_target_job_satisfaction_rate - НЕТ

Проверка датафреймов на неявные дубликаты¶

In [10]:
for name, data in dataframes.items():
    col_cat = data.select_dtypes(include=['object']).columns.to_list()
    print(f'\033[1mНазвание датафрейма {name}\033[0m')
    for col in col_cat:
        print(f"Уникальные значения в столбце '{col}': {data[col].unique()}")
    print('=' * TERM_SIZE.columns)
Название датафрейма train_job_satisfaction_rate
Уникальные значения в столбце 'dept': ['sales' 'hr' 'technology' 'purchasing' 'marketing' nan]
Уникальные значения в столбце 'level': ['junior' 'middle' 'sinior' nan]
Уникальные значения в столбце 'workload': ['medium' 'high' 'low']
Уникальные значения в столбце 'last_year_promo': ['no' 'yes']
Уникальные значения в столбце 'last_year_violations': ['no' 'yes']
===============================================================================================================
Название датафрейма test_features
Уникальные значения в столбце 'dept': ['marketing' 'hr' 'sales' 'purchasing' 'technology' nan ' ']
Уникальные значения в столбце 'level': ['junior' 'middle' 'sinior' nan]
Уникальные значения в столбце 'workload': ['medium' 'low' 'high' ' ']
Уникальные значения в столбце 'last_year_promo': ['no' 'yes']
Уникальные значения в столбце 'last_year_violations': ['no' 'yes']
===============================================================================================================
Название датафрейма test_target_job_satisfaction_rate
===============================================================================================================

Заменим пропуски датафрейма test_features на значение Nan

In [11]:
test_features['dept'] = test_features['dept'].replace(' ', np.nan)
test_features['workload'] = test_features['workload'].replace(' ', np.nan)
test_features['level'] = test_features['level'].replace('sinior', 'senior')

Исправление опечатки в наименовании должности senior

In [12]:
train_job_satisfaction_rate['level'] = train_job_satisfaction_rate['level'].replace('sinior', 'senior')
test_features['level'] = test_features['level'].replace('sinior', 'senior')
In [13]:
col_cat = test_features.select_dtypes(include=['object']).columns.to_list()
for col in col_cat:
    print(f"Уникальные значения в столбце '{col}': {test_features[col].unique()}")
Уникальные значения в столбце 'dept': ['marketing' 'hr' 'sales' 'purchasing' 'technology' nan]
Уникальные значения в столбце 'level': ['junior' 'middle' 'senior' nan]
Уникальные значения в столбце 'workload': ['medium' 'low' 'high' nan]
Уникальные значения в столбце 'last_year_promo': ['no' 'yes']
Уникальные значения в столбце 'last_year_violations': ['no' 'yes']
In [14]:
# Обновление словаря с данными
dataframes = {
    "train_job_satisfaction_rate": train_job_satisfaction_rate,
    "test_features": test_features,
    "test_target_job_satisfaction_rate": test_target_job_satisfaction_rate
}


for name, data in dataframes.items():
    print(f"Явных дубликатов в датасете {name} - {data.duplicated().sum()}: "
          if data.duplicated().sum() > 0 else f"Явных дубликатов в датасете {name} - НЕТ")
Явных дубликатов в датасете train_job_satisfaction_rate - НЕТ
Явных дубликатов в датасете test_features - НЕТ
Явных дубликатов в датасете test_target_job_satisfaction_rate - НЕТ

Вывод по предобработке данных:

В ходе предобработке данных была выполненна проверка датафреймов на дубликаты и пропуски, в результате которой все пропуски были заменены на значение Nan, опечатка в слове senior исправлена

Исследовательский анализ данных¶

Функции для исследовательского анализа данных¶

In [15]:
def show_num_variable(df, column, target=None, suptitle=None):
    '''
    Функция отображения гистограммы распределения
    и диаграммы размаха для определенного столбца датафрейма
    с учетом принадлежности данного столбца к разным значениям
    переменной target.
    
    Параметры:
    - df: pandas.DataFrame, входной датафрейм
    - column: str, столбец для анализа
    - target: str или None, столбец для группировки (по умолчанию None)
    '''
    sns.set()
    f, axes = plt.subplots(1, 2, figsize=(16, 6))
    
    # Гистограмма
    axes[0].set_title(f'Гистограмма для {column}', fontsize=16)
    if target:
        axes[0].set_ylabel('Плотность', fontsize=14)
        sns.histplot(data=df, bins=20, kde=True, ax=axes[0], hue=target, x=column, stat='density', common_norm=False)
    else:
        axes[0].set_ylabel('Количество', fontsize=14)
        sns.histplot(data=df, bins=20, kde=True, ax=axes[0], x=column)
    
    # Диаграмма размаха
    axes[1].set_title(f'Диаграмма размаха для {column}', fontsize=16)
    if target:
        sns.boxplot(data=df, ax=axes[1], x=target, y=column)
    else:
        sns.boxplot(data=df, ax=axes[1], y=column, orient='v')
        axes[1].set_ylabel(column, fontsize=14)

    # Добавляем главный заголовок
    if suptitle:
        plt.suptitle(f'{suptitle}', fontsize=18, fontweight='bold')
    
    plt.tight_layout()
    plt.show()
In [16]:
def show_discrete_variable(df, column, target=None):
    '''
    Функция отображения гистограммы распределения для дискретного столбца датафрейма.
    Если задан target, строит отдельные графики для каждой группы.
    
    Параметры:
    - df: pandas.DataFrame, входной датафрейм
    - column: str, столбец для анализа (категориальный)
    - target: str или None, целевая переменная для разбиения (по умолчанию None)
    '''

    # Проверяем наличие столбцов в датафрейме
    if column not in df.columns:
        raise ValueError(f"Столбец '{column}' отсутствует в датафрейме.")
    if target and target not in df.columns:
        raise ValueError(f"Столбец '{target}' отсутствует в датафрейме.")

    # Если целевой переменной нет, строим обычный countplot
    if target is None:
        plt.figure(figsize=(10, 6))
        plt.title(f'Распределение значений {column}', fontsize=16)
        ax = sns.countplot(data=df, x=column)
        add_percentages(ax, df, column)  # Добавляем проценты
        ax.set_ylabel("Частота (%)", fontsize=14)
        plt.show()
        return

    # Получаем уникальные значения целевой переменной
    unique_values = df[target].unique()

    # Создаём два графика рядом
    fig, axes = plt.subplots(1, len(unique_values), figsize=(12, 6), sharey=True)

    for ax, value in zip(axes, unique_values):
        subset = df[df[target] == value]  # Выборка по значению target
        ax.set_title(f"{target} = {value}", fontsize=14)
        sns.countplot(data=subset, x=column, ax=ax)
        add_percentages(ax, subset, column)  # Добавляем проценты
        ax.set_xlabel(column, fontsize=12)
    
    axes[0].set_ylabel("Частота (%)", fontsize=14)  # ось Y
    axes[1].set_ylabel("Частота (%)", fontsize=14)  # ось Y
    plt.tight_layout()
    plt.show()


def add_percentages(ax, df, column):
    """Функция для добавления процентов над столбцами"""
    total = len(df)
    for p in ax.patches:
        count = p.get_height()
        if count > 0:
            percentage = f'{100 * count / total:.1f}%'
            ax.annotate(percentage, 
                        (p.get_x() + p.get_width() / 2, p.get_height()), 
                        ha='center', va='bottom', fontsize=12, color='black')
In [17]:
def show_cat_variable_by_target(df, column, title, target=None, rot=60):
    '''
    Функция отображения соотношения категориальных признаков
    в столбце датафрейма, разделенных по значениям целевого признака.
    Если target не передан, отображается только countplot для column.
    Добавляет проценты на график.
    
    :param df: DataFrame, датафрейм с данными
    :param column: str, название столбца, по которому строится график
    :param title: str, заголовок графика
    :param target: str, название столбца с целевым признаком (по умолчанию None)
    :param rot: int, угол поворота меток на оси X
    '''
    
    # Если target не указан, рисуем только countplot для одного признака
    if target is None:
        plt.figure(figsize=(12, 6))
        ax = sns.countplot(data=df, x=column)
        ax.set_title(f"{title}", fontsize=14)
        ax.set_xlabel(column, fontsize=12)
        ax.set_ylabel('Количество', fontsize=12)
        ax.tick_params(axis='x', rotation=rot)
        
        # Добавляем проценты на график
        total_count = len(df)
        for p in ax.patches:
            count = p.get_height()
            if count > 0:  # Добавляем проценты только если высота столбца > 0
                percentage = f'{100 * count / total_count:.1f}%'
                ax.annotate(percentage,
                            (p.get_x() + p.get_width() / 2., count),
                            ha='center', va='center', 
                            xytext=(0, 10), 
                            textcoords='offset points',
                            fontsize=12, color='black')
        
        plt.tight_layout()
        plt.show()
        return
    
    # Проверяем, что столбец target существует в DataFrame
    if target not in df.columns:
        raise ValueError(f"Столбец '{target}' не найден в DataFrame.")
    
    # Если target указан, строим графики для каждого уникального значения target
    unique_targets = df[target].unique()
    num_targets = len(unique_targets)
    
    # Определяем размер холста в зависимости от количества таргетов
    fig, axes = plt.subplots(nrows=num_targets, ncols=1, figsize=(12, 5 * num_targets), sharex=True)
    
    # Если уникальных значений больше одного, axes будет массивом
    # Если только одно значение, делаем axes списком для унификации
    if num_targets == 1:
        axes = [axes]
    
    # Создаем график для каждого уникального значения целевого признака
    for i, target_value in enumerate(unique_targets):
        # Создаем подмножество данных для текущего значения целевого признака
        subset = df[df[target] == target_value]
        
        # Получаем уникальные значения категориального признака для сортировки
        categories = subset[column].value_counts().index.tolist()
        
        # Создаем countplot для текущего значения целевого признака
        ax = sns.countplot(data=subset, x=column, ax=axes[i], order=categories)
        
        # Заголовок и настройка осей
        axes[i].set_title(f"{title}: {target} - {target_value}", fontsize=14)
        axes[i].set_xlabel(column if i == num_targets - 1 else "", fontsize=12)  # Подпись только для нижнего графика
        axes[i].set_ylabel('Доля сотрудников', fontsize=12)
        axes[i].tick_params(axis='x', rotation=rot)
        
        # Вычисляем общее количество для текущей группы
        total_count = len(subset)
        
        # Добавляем проценты на столбцы
        for p in ax.patches:
            count = p.get_height()
            if count > 0:  # Добавляем проценты только если высота столбца > 0
                percentage = f'{100 * count / total_count:.1f}%'
                # Вычисляем координаты для аннотации (верхняя часть столбца)
                ax.annotate(percentage,
                            (p.get_x() + p.get_width() / 2., count),
                            ha='center', va='center', 
                            xytext=(0, 10), 
                            textcoords='offset points',
                            fontsize=12, color='black')
    
    # Улучшаем отображение
    plt.tight_layout(h_pad=2.0)  # Добавляем вертикальный отступ между графиками
    plt.show()
In [18]:
def normal_check(data, column, alpha=0.05):
    '''
    Функция проверки нормальности распределения
    по тесту Шапиро — Уилка
    с обработкой больших наборов данных.
    '''

    print(f"""
        Выдвенем гипотезы:
        - Н0: Распределение параметра {column} является нормальным.
        - Н1: Распределение параметра {column} не является нормальным.
    """)

    sample = data[column]

    # Тест Шапиро-Уилка
    stat, p = shapiro(sample)
    print(f"Тест Шапиро — Уилка: Stat={stat:.3f}, p={p:.3g}")

    # Результат
    if p > alpha:
        return print(f"Распределение данных нормальное с вероятностью более {1 - alpha:.2f}. Не получилось отвергнуть нулевую гипотезу")
    else:
        return print(f"Распределение данных не нормальное с вероятностью более {1 - alpha:.2f}. Отвергаем нулевую гипотезу")

Объединение тестовой выборки для статистического анализа¶

In [19]:
test_full = test_features.merge(test_target_job_satisfaction_rate, on='id', how='inner')
display(test_full.shape)
test_full.sample(5)
(2000, 10)
Out[19]:
id dept level workload employment_years last_year_promo last_year_violations supervisor_evaluation salary job_satisfaction_rate
695 116186 technology junior medium 1 no no 4 26400 0.55
1203 393065 purchasing junior high 1 no no 2 37200 0.27
1953 196670 sales middle high 7 no no 2 45600 0.29
530 612023 purchasing junior medium 1 no no 3 25200 0.31
992 308908 technology middle low 7 no no 3 21600 0.32

Анализ количественных и качественных признаков¶

Для тренировочных данных train_job_satisfaction_rate¶
In [20]:
# Формирование списка столбцов с количественными признаками
num_variables_col = train_job_satisfaction_rate.select_dtypes(include=['number']).columns.to_list()
num_variables_col = [col for col in num_variables_col if col not in ['id', 'supervisor_evaluation', 'employment_years']]
num_variables_col
Out[20]:
['salary', 'job_satisfaction_rate']
In [21]:
# Формирование списка столбцов с количественными дискретными признаками
discrete_variables_col = ['supervisor_evaluation', 'employment_years']
discrete_variables_col
Out[21]:
['supervisor_evaluation', 'employment_years']
In [22]:
# Вывод графиков для датафрейма train_job_satisfaction_rate
for col in num_variables_col:
    show_num_variable(train_job_satisfaction_rate, col)
    normal_check(train_job_satisfaction_rate, col)
    print('=' * TERM_SIZE.columns)
No description has been provided for this image
        Выдвенем гипотезы:
        - Н0: Распределение параметра salary является нормальным.
        - Н1: Распределение параметра salary не является нормальным.
    
Тест Шапиро — Уилка: Stat=0.939, p=8.39e-38
Распределение данных не нормальное с вероятностью более 0.95. Отвергаем нулевую гипотезу
===============================================================================================================
No description has been provided for this image
        Выдвенем гипотезы:
        - Н0: Распределение параметра job_satisfaction_rate является нормальным.
        - Н1: Распределение параметра job_satisfaction_rate не является нормальным.
    
Тест Шапиро — Уилка: Stat=0.971, p=8.95e-28
Распределение данных не нормальное с вероятностью более 0.95. Отвергаем нулевую гипотезу
===============================================================================================================
In [23]:
train_job_satisfaction_rate[num_variables_col].describe().round(3).T
Out[23]:
count mean std min 25% 50% 75% max
salary 4000.0 33926.700 14900.704 12000.00 22800.00 30000.00 43200.00 98400.0
job_satisfaction_rate 4000.0 0.534 0.225 0.03 0.36 0.56 0.71 1.0

Вывод для количесвенных значений датафрейма train_job_satisfaction_rate:

  • Распределения количественных значений датафрейма train_job_satisfaction_rate отличаются от Гауссовсского;
  • Для salary (ежемесячная заработная плата) наиболее популярны значения от 20000 до 30000. Так же были замечены выбросы с зарплатами более 73800 для отдельных сеньеров. Среднее значение salary - mean = 33927, медианное значение salary - median = 30000;
  • Для job_satisfaction_rate (уровень удовлетворённости сотрудника работой в компании) наиболее популярны значения от 0.6 до 0.8. Среднее значение job_satisfaction_rate - mean = 0.534, медианное значение job_satisfaction_rate - median = 0.56.
In [24]:
# Вывод графиков для дискретных значений датафрейма train_job_satisfaction_rate
for col in discrete_variables_col:
    show_discrete_variable(train_job_satisfaction_rate, col)
    print('=' * TERM_SIZE.columns)
No description has been provided for this image
===============================================================================================================
No description has been provided for this image
===============================================================================================================
In [25]:
train_job_satisfaction_rate[discrete_variables_col].describe().round(3).T
Out[25]:
count mean std min 25% 50% 75% max
supervisor_evaluation 4000.0 3.476 1.009 1.0 3.0 4.0 4.0 5.0
employment_years 4000.0 3.718 2.543 1.0 2.0 3.0 6.0 10.0

Вывод для дискретных признаков в датафрейма train_job_satisfaction_rate:

  • Для supervisor_evaluation (оценка качества работы сотрудника) наиболее популярные оценки 3 и 4. Среднее значение supervisor_evaluation - mean = 3.476, медианное значение supervisor_evaluation - median = 4. Так же в ходе анализа было найдено отсутствие зависимости качество работы сотрудника от занимаемой им должности;
  • Для employment_years (длительности работы в компании) наблюдается, что наибольшее количество работников проработали в компании 1 год, в ней также отсутствуют работники без опыта, в целом заметен ниспадающий тренд чем больше стаж тем меньше работников данного стажа в компании. Среднее значение employment_years - mean = 3.718, медианное значение employment_years - median = 3.

Выведем строки выбросов по salary

In [26]:
# Вычисляем квартили и интерквартильный размах (IQR)
Q1 = train_job_satisfaction_rate['salary'].quantile(0.25)  # Первый квартиль
Q3 = train_job_satisfaction_rate['salary'].quantile(0.75)  # Третий квартиль
IQR = Q3 - Q1  # Интерквартильный размах

# Верхняя граница диаграмы размаха
upper_bound = Q3 + 1.5 * IQR

display(upper_bound)

top_salary = train_job_satisfaction_rate.query('salary > @upper_bound')
display(top_salary.shape[0])
top_salary.sample(10)
73800.0
60
Out[26]:
id dept level workload employment_years last_year_promo last_year_violations supervisor_evaluation salary job_satisfaction_rate
2717 681745 technology senior high 5 no yes 3 76800 0.03
3150 811299 marketing senior high 8 no no 4 75600 0.64
3570 498123 purchasing senior high 8 no no 4 79200 0.88
1982 978915 technology senior high 7 no no 1 92400 0.45
3571 562085 sales senior high 1 no no 3 75600 0.41
3016 147589 technology senior high 2 no no 1 75600 0.16
3828 214696 sales senior high 5 no yes 3 75600 0.13
2932 335600 marketing senior high 4 no no 3 79200 0.31
1979 701051 sales senior high 2 no no 3 78000 0.37
2146 229741 sales senior high 2 no no 5 79200 0.66
In [27]:
top_salary['level'].unique()
Out[27]:
array(['senior'], dtype=object)

Зависимость заработной платы от должности

In [28]:
show_num_variable(train_job_satisfaction_rate, 'salary', 'level')
No description has been provided for this image

Из гистограммы распределения плотности зарплат для разных должностей можно заметить связь значения зп от занимаемой должности.

Определим зависимость оценки качества работы сотрудника от его должности

In [29]:
train_job_satisfaction_rate.groupby('level')['supervisor_evaluation'].describe().round(3).T
Out[29]:
level junior middle senior
count 1894.00 1744.000 358.000
mean 3.48 3.481 3.430
std 1.01 1.006 1.015
min 1.00 1.000 1.000
25% 3.00 3.000 3.000
50% 4.00 4.000 4.000
75% 4.00 4.000 4.000
max 5.00 5.000 5.000
In [30]:
# Лист с названиями столбцов категориальных признаков
dict_market_file_cat = train_job_satisfaction_rate.select_dtypes(include=['object']).columns.to_list()

for col in dict_market_file_cat:
    show_cat_variable_by_target(train_job_satisfaction_rate, col, col)
    print('=' * TERM_SIZE.columns)
No description has been provided for this image
===============================================================================================================
No description has been provided for this image
===============================================================================================================
No description has been provided for this image
===============================================================================================================
No description has been provided for this image
===============================================================================================================
No description has been provided for this image
===============================================================================================================

Вывод по категориальным данным датафрейма train_data_without_id:

  • Больше всего сотрудников в компании работает в продажах (37.8 %), наименьшее число работает в hr;
  • В компании малое количество высококвалифицированных сотрудников (senior = 8.9 %), сотрудников низкой и средней квалификации примерно равное количество (junior = 47.4 %, middle = 43.6 %);
  • Большинство сотрудников в компании имеют среднюю загруженность (их 51.6 %), наименьшее количество высокозагруженных работников (их 18.4 %);
  • В компании редко происходят повышения, за прошлый год повысили только 3 % сотрудников;
  • Так же большинство сотрудников соблюдают правила установленные компанией в трудовом договоре (их 86 %).

Корреляционный анализ

Для корреляционного анализа и впоследствии модели машинного обучения, признак id не нужен, т.к. не несет никакой смысловой нагрузки, а так же он может повлиять на итоговую модель.

In [31]:
# Удаление столбца id
train_data_without_id = train_job_satisfaction_rate.drop('id', axis=1)
In [32]:
# Создание списка столбцов с непрерывно распределенными данными
col_names_corr = ['salary', 'job_satisfaction_rate']

big_data_corr = train_data_without_id.phik_matrix(interval_cols=col_names_corr)

plt.figure(figsize=(14, 10))
sns.heatmap(big_data_corr, annot=True, fmt=".2f", center=0)
plt.title('Матрица корреляций phik для датафрейма train_job_satisfaction_rate')
plt.show()
No description has been provided for this image

Вывод по корреляционному анализу для датафрейма train_job_satisfaction_rate

Мультиколлеанарности между признаками не замечено;

В ходе анализа Phik матрицы корреляций были определены силы связи целевого признака job_satisfaction_rate со входными, используя шкалу Чеддока:

  • Высокая связь целевого признака прослеживается с входным признаком supervisor_evaluation (0.76);
  • Средняя связь целевого признака прослеживается с входным признаком last_year_violations (0.56):
  • Слабая связь целевого признака прослеживается с входным признаком employment_years (0.33);
  • С остальными входными признаками связь очень слабая.
Для тестовых данных test_full¶
In [33]:
# Формирование списка столбцов с количественными признаками
num_variables_col = test_full.select_dtypes(include=['number']).columns.to_list()
num_variables_col = [col for col in num_variables_col if col not in ['id', 'supervisor_evaluation', 'employment_years']]
num_variables_col
Out[33]:
['salary', 'job_satisfaction_rate']
In [34]:
# Формирование списка столбцов с количественными дискретными признаками
discrete_variables_col = ['supervisor_evaluation', 'employment_years']
discrete_variables_col
Out[34]:
['supervisor_evaluation', 'employment_years']
In [35]:
# Вывод графиков для датафрейма test_full
for col in num_variables_col:
    show_num_variable(test_full, col)
    normal_check(test_full, col)
    print('=' * TERM_SIZE.columns)
No description has been provided for this image
        Выдвенем гипотезы:
        - Н0: Распределение параметра salary является нормальным.
        - Н1: Распределение параметра salary не является нормальным.
    
Тест Шапиро — Уилка: Stat=0.926, p=2.13e-30
Распределение данных не нормальное с вероятностью более 0.95. Отвергаем нулевую гипотезу
===============================================================================================================
No description has been provided for this image
        Выдвенем гипотезы:
        - Н0: Распределение параметра job_satisfaction_rate является нормальным.
        - Н1: Распределение параметра job_satisfaction_rate не является нормальным.
    
Тест Шапиро — Уилка: Stat=0.970, p=5.12e-20
Распределение данных не нормальное с вероятностью более 0.95. Отвергаем нулевую гипотезу
===============================================================================================================
In [36]:
test_full[num_variables_col].describe().round(3).T
Out[36]:
count mean std min 25% 50% 75% max
salary 2000.0 34066.800 15398.437 12000.00 22800.00 30000.00 43200.00 96000.0
job_satisfaction_rate 2000.0 0.549 0.220 0.03 0.38 0.58 0.72 1.0

Вывод для количесвенных значений датафрейма test_full:

  • Распределения количественных значений датафрейма test_full отличаются от Гауссовсского;
  • Для salary (ежемесячная заработная плата) наиболее популярны значения от 20000 до 30000. Так же были замечены выбросы с зарплатами более 73800 для отдельных сеньеров. Среднее значение salary - mean = 34067, медианное значение salary - median = 30000;
  • Для job_satisfaction_rate (уровень удовлетворённости сотрудника работой в компании) наиболее популярны значения от 0.6 до 0.8. Среднее значение job_satisfaction_rate - mean = 0.549, медианное значение job_satisfaction_rate - median = 0.58.
In [37]:
# Вывод графиков для дискретных значений датафрейма test_full
for col in discrete_variables_col:
    show_discrete_variable(test_full, col)
    print('=' * TERM_SIZE.columns)
No description has been provided for this image
===============================================================================================================
No description has been provided for this image
===============================================================================================================
In [38]:
test_full[discrete_variables_col].describe().round(3).T
Out[38]:
count mean std min 25% 50% 75% max
supervisor_evaluation 2000.0 3.526 0.997 1.0 3.0 4.0 4.0 5.0
employment_years 2000.0 3.666 2.537 1.0 1.0 3.0 6.0 10.0

Вывод для дискретных признаков в датафрейма test_full:

  • Для supervisor_evaluation (оценка качества работы сотрудника) наиболее популярные оценки 3 и 4. Среднее значение supervisor_evaluation - mean = 3.526, медианное значение supervisor_evaluation - median = 4. Так же в ходе анализа было найдено отсутствие зависимости качество работы сотрудника от занимаемой им должности;
  • Для employment_years (длительности работы в компании) наблюдается, что наибольшее количество работников проработали в компании 1 год, в ней также отсутствуют работники без опыта, в целом заметен ниспадающий тренд чем больше стаж тем меньше работников данного стажа в компании. Среднее значение employment_years - mean = 3.666, медианное значение employment_years - median = 3.
In [39]:
# Лист с названиями столбцов категориальных признаков
dict_market_file_cat = test_full.select_dtypes(include=['object']).columns.to_list()

for col in dict_market_file_cat:
    show_cat_variable_by_target(test_full, col, col)
    print('=' * TERM_SIZE.columns)
No description has been provided for this image
===============================================================================================================
No description has been provided for this image
===============================================================================================================
No description has been provided for this image
===============================================================================================================
No description has been provided for this image
===============================================================================================================
No description has been provided for this image
===============================================================================================================

Вывод для категориальных признаков в датафрейма test_full:

  • Больше всего сотрудников в компании работает в продажах (38.1%), наименьшее число работает в hr;
  • В компании малое количество высококвалифицированных сотрудников (senior = 8.8%), сотрудников низкой и средней квалификации примерно равное количество (junior = 48.7%, middle = 42.7%);
  • Большинство сотрудников в компании имеют среднюю загруженность (их 52.1 %), наименьшее количество высокозагруженных работников (их 18.1%);
  • В компании редко происходят повышения, за прошлый год повысили только 3.1 % сотрудников;
  • Так же большинство сотрудников соблюдают правила установленные компанией в трудовом договоре (их 86.9%).

Корреляционный анализ¶

Для корреляционного анализа и впоследствии модели машинного обучения, признак id не нужен, т.к. не несет никакой смысловой нагрузки, а так же он может повлиять на итоговую модель.

In [40]:
# Удаление столбца id
test_data_without_id = test_full.drop('id', axis=1)
In [41]:
# Создание списка столбцов с непрерывно распределенными данными
col_names_corr = ['salary', 'job_satisfaction_rate']

big_data_corr = test_data_without_id.phik_matrix(interval_cols=col_names_corr)

plt.figure(figsize=(14, 10))
sns.heatmap(big_data_corr, annot=True, fmt=".2f", center=0)
plt.title('Матрица корреляций phik для датафрейма test_full')
plt.show()
No description has been provided for this image

Вывод по корреляционному анализу для датафрейма test_full

Мультиколлеанарности между признаками не замечено;

В ходе анализа Phik матрицы корреляций были определены силы связи целевого признака job_satisfaction_rate со входными, используя шкалу Чеддока:

  • Высокая связь целевого признака прослеживается с входным признаком supervisor_evaluation (0.77);
  • Средняя связь целевого признака прослеживается с входным признаком last_year_violations (0.55):
  • Слабая связь целевого признака прослеживается с входным признаком employment_years (0.31) и last_year_promo;
  • С остальными входными признаками связь очень слабая.

Общий вывод по исследовательскому анализу

Для тренировочной выборки

  • Распределения количественных значений датафрейма train_job_satisfaction_rate отличаются от Гауссовсского;

  • Для salary (ежемесячная заработная плата) наиболее популярны значения от 20000 до 30000. Так же были замечены выбросы с зарплатами более 73800 для отдельных сеньеров. Среднее значение salary - mean = 33927, медианное значение salary - median = 30000;

  • Для job_satisfaction_rate (уровень удовлетворённости сотрудника работой в компании) наиболее популярны значения от 0.6 до 0.8. Среднее значение job_satisfaction_rate - mean = 0.534, медианное значение job_satisfaction_rate - median = 0.56.

  • Для supervisor_evaluation (оценка качества работы сотрудника) наиболее популярные оценки 3 и 4. Среднее значение supervisor_evaluation - mean = 3.476, медианное значение supervisor_evaluation - median = 4. Так же в ходе анализа было найдено отсутствие зависимости качество работы сотрудника от занимаемой им должности;

  • Для employment_years (длительности работы в компании) наблюдается, что наибольшее количество работников проработали в компании 1 год, в ней также отсутствуют работники без опыта, в целом заметен ниспадающий тренд чем больше стаж тем меньше работников данного стажа в компании. Среднее значение employment_years - mean = 3.718, медианное значение employment_years - median = 3.

  • Больше всего сотрудников в компании работает в продажах (37.8 %), наименьшее число работает в hr;

  • В компании малое количество высококвалифицированных сотрудников (senior = 8.9 %), сотрудников низкой и средней квалификации примерно равное количество (junior = 47.4 %, middle = 43.6 %);

  • Большинство сотрудников в компании имеют среднюю загруженность (их 51.6 %), наименьшее количество высокозагруженных работников (их 18.4 %);

  • В компании редко происходят повышения, за прошлый год повысили только 3 % сотрудников;

  • Так же большинство сотрудников соблюдают правила установленные компанией в трудовом договоре (их 86 %).

Мультиколлеанарности между признаками не замечено;

В ходе анализа Phik матрицы корреляций были определены силы связи целевого признака job_satisfaction_rate со входными, используя шкалу Чеддока:

  • Высокая связь целевого признака прослеживается с входным признаком supervisor_evaluation (0.76);
  • Средняя связь целевого признака прослеживается с входным признаком last_year_violations (0.56):
  • Слабая связь целевого признака прослеживается с входным признаком employment_years (0.33);
  • С остальными входными признаками связь очень слабая.

Для тестовой выборки

  • Распределения количественных значений датафрейма test_full отличаются от Гауссовсского;

  • Для salary (ежемесячная заработная плата) наиболее популярны значения от 20000 до 30000. Так же были замечены выбросы с зарплатами более 73800 для отдельных сеньеров. Среднее значение salary - mean = 34067, медианное значение salary - median = 30000;

  • Для job_satisfaction_rate (уровень удовлетворённости сотрудника работой в компании) наиболее популярны значения от 0.6 до 0.8. Среднее значение job_satisfaction_rate - mean = 0.549, медианное значение job_satisfaction_rate - median = 0.58.

  • Для supervisor_evaluation (оценка качества работы сотрудника) наиболее популярные оценки 3 и 4. Среднее значение supervisor_evaluation - mean = 3.526, медианное значение supervisor_evaluation - median = 4. Так же в ходе анализа было найдено отсутствие зависимости качество работы сотрудника от занимаемой им должности;

  • Для employment_years (длительности работы в компании) наблюдается, что наибольшее количество работников проработали в компании 1 год, в ней также отсутствуют работники без опыта, в целом заметен ниспадающий тренд чем больше стаж тем меньше работников данного стажа в компании. Среднее значение employment_years - mean = 3.666, медианное значение employment_years - median = 3.

  • Больше всего сотрудников в компании работает в продажах (38.1%), наименьшее число работает в hr;

  • В компании малое количество высококвалифицированных сотрудников (senior = 8.8%), сотрудников низкой и средней квалификации примерно равное количество (junior = 48.7%, middle = 42.7%);

  • Большинство сотрудников в компании имеют среднюю загруженность (их 52.1 %), наименьшее количество высокозагруженных работников (их 18.1%);

  • В компании редко происходят повышения, за прошлый год повысили только 3.1 % сотрудников;

  • Так же большинство сотрудников соблюдают правила установленные компанией в трудовом договоре (их 86.9%).

Мультиколлеанарности между признаками не замечено;

В ходе анализа Phik матрицы корреляций были определены силы связи целевого признака job_satisfaction_rate со входными, используя шкалу Чеддока:

  • Высокая связь целевого признака прослеживается с входным признаком supervisor_evaluation (0.77);
  • Средняя связь целевого признака прослеживается с входным признаком last_year_violations (0.55):
  • Слабая связь целевого признака прослеживается с входным признаком employment_years (0.31) и last_year_promo;
  • С остальными входными признаками связь очень слабая.

В тестовой выборке на целевой признак job_satisfaction_rate входной last_year_promo 0.34 по сравнению с тренировочной 0.19. В остальном данные схожи поэтому: Отличие тренировочной и тестовой выборки не выявлено, данные можно использовать для МО

Подготовка данных¶

In [42]:
# Тренировочные данные
X_train_1 = train_job_satisfaction_rate.drop(['job_satisfaction_rate', 'id'], axis=1)
In [43]:
y_train_1 = train_job_satisfaction_rate['job_satisfaction_rate']
In [44]:
# Тестовые данные
X_test_1 = test_full.drop(['job_satisfaction_rate', 'id'], axis=1)
y_test_1 = test_full['job_satisfaction_rate']
In [45]:
# Определение числовых и текстовых признаков
num_columns = X_train_1.select_dtypes(include=['int64', 'float64']).columns.tolist()
ohe_columns = X_train_1.select_dtypes(include=['object']).columns.tolist()
ohe_columns = [col for col in ohe_columns if col not in ['level', 'workload']]
ord_columns = ['level', 'workload']
display(ohe_columns)
['dept', 'last_year_promo', 'last_year_violations']

Вывод по подготовке данных:

Данные были разделены на тренировочную и тестовую выборку и подготовлены для дальнейшего обучения.

Обучение модели¶

Перечислим особенности данных:

  1. Три признака:
    dept, last_year_promo, last_year_violations — нужно кодировать с помощью OneHotEncoder.
  2. Два признака: level, workload — нужно кодировать с помощью OrdinalEncoder.
  3. Три Количественных признака:
    employment_years, supervisor_evaluation, salary - нужно масштабировать.
  4. В признаках пропуски встречаются и обработаем их в пайплайне.
  5. Целевой признак — job_satisfaction_rate.
In [46]:
# создаём пайплайн для подготовки признаков из списка ohe_columns: заполнение пропусков и OHE-кодирование
# SimpleImputer + OHE
ohe_pipe = Pipeline(
    [
        (
            'simpleImputer_ohe', 
            SimpleImputer(missing_values=np.nan, strategy='most_frequent')
        ),
        (
            'ohe', 
            OneHotEncoder(drop='first', handle_unknown='ignore', sparse_output=False)
        )
    ]
)


ord_pipe = Pipeline(
    [
        (
            'simpleImputer_before_ord', 
            SimpleImputer(missing_values=np.nan, strategy='most_frequent')
        ),
        (
            'ord',  
            OrdinalEncoder(
                categories=[
                    ['junior', 'middle', 'sinior'], 
                    ['low', 'medium', 'high']
                ], 
                handle_unknown='use_encoded_value', unknown_value=np.nan
            )
        ),
        (
            'simpleImputer_after_ord', 
            SimpleImputer(missing_values=np.nan, strategy='most_frequent')
        )
    ]
) 


# создаём общий пайплайн для подготовки данных
data_preprocessor = ColumnTransformer(
    [
        ('ohe', ohe_pipe, ohe_columns),
        ('ord', ord_pipe, ord_columns),
        ('num', MinMaxScaler(), num_columns)
    ], 
    remainder='passthrough'
)


pipe_final = Pipeline(
    [
        ('preprocessor', data_preprocessor),
        ('model', DecisionTreeRegressor(random_state=RANDOM_STATE))
    ]
)


# Сетка гиперпараметров
param_grid = [
    # Сетка для DecisionTreeRegressor
    {
        'model': [DecisionTreeRegressor(random_state=RANDOM_STATE)],
        'model__max_depth': range(2, 15),
        'model__max_features': range(2, len(num_columns) + len(ohe_columns) + len(ord_columns)),  # Количество признаков
        'preprocessor__num': [StandardScaler(), MinMaxScaler(), 'passthrough'],
    },
    # Сетка для SVR
    {
        'model': [SVR()],
        'model__C': [0.1, 1, 10],
        'model__gamma': ['scale', 'auto', 0.1, 1],
        'preprocessor__num': [StandardScaler(), MinMaxScaler(), 'passthrough']
    },
    # Сетка для LinearRegression
    {
        'model': [LinearRegression()],
        'preprocessor__num': [StandardScaler(), MinMaxScaler(), 'passthrough']
    },
     # DummyRegressor
    {
        'model': [DummyRegressor()],
        'model__strategy': ['mean']
    }
]
In [47]:
def smape(y_true, y_pred):
    """
    Вычисляет симметричную среднюю абсолютную процентную ошибку (SMAPE).

    Формула:
        SMAPE = (100 / N) * Σ(2 * |y_pred - y_true| / (|y_true| + |y_pred|))

    Параметры:
    ----------
    y_true : array
        Истинные значения целевой переменной.

    y_pred : array
        Предсказанные значения целевой переменной.

    Возвращает:
    ----------
    float
        Значение SMAPE (в процентах).:"""
    return 100/len(y_true) * np.sum(2 * np.abs(y_pred - y_true) / (np.abs(y_true) + np.abs(y_pred)))
   
smape_scorer = make_scorer(score_func=smape, greater_is_better=False)
In [48]:
grid_search = GridSearchCV(
    pipe_final,
    param_grid,
    n_jobs=-1,
    cv=5,
    scoring=smape_scorer
)
grid_search.fit(X_train_1, y_train_1)
Out[48]:
GridSearchCV(cv=5,
             estimator=Pipeline(steps=[('preprocessor',
                                        ColumnTransformer(remainder='passthrough',
                                                          transformers=[('ohe',
                                                                         Pipeline(steps=[('simpleImputer_ohe',
                                                                                          SimpleImputer(strategy='most_frequent')),
                                                                                         ('ohe',
                                                                                          OneHotEncoder(drop='first',
                                                                                                        handle_unknown='ignore',
                                                                                                        sparse_output=False))]),
                                                                         ['dept',
                                                                          'last_year_promo',
                                                                          'last_year_violations']),
                                                                        ('ord',
                                                                         Pipeline(...
                         {'model': [SVR()], 'model__C': [0.1, 1, 10],
                          'model__gamma': ['scale', 'auto', 0.1, 1],
                          'preprocessor__num': [StandardScaler(),
                                                MinMaxScaler(),
                                                'passthrough']},
                         {'model': [LinearRegression()],
                          'preprocessor__num': [StandardScaler(),
                                                MinMaxScaler(),
                                                'passthrough']},
                         {'model': [DummyRegressor()],
                          'model__strategy': ['mean']}],
             scoring=make_scorer(smape, greater_is_better=False, response_method='predict'))
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
GridSearchCV(cv=5,
             estimator=Pipeline(steps=[('preprocessor',
                                        ColumnTransformer(remainder='passthrough',
                                                          transformers=[('ohe',
                                                                         Pipeline(steps=[('simpleImputer_ohe',
                                                                                          SimpleImputer(strategy='most_frequent')),
                                                                                         ('ohe',
                                                                                          OneHotEncoder(drop='first',
                                                                                                        handle_unknown='ignore',
                                                                                                        sparse_output=False))]),
                                                                         ['dept',
                                                                          'last_year_promo',
                                                                          'last_year_violations']),
                                                                        ('ord',
                                                                         Pipeline(...
                         {'model': [SVR()], 'model__C': [0.1, 1, 10],
                          'model__gamma': ['scale', 'auto', 0.1, 1],
                          'preprocessor__num': [StandardScaler(),
                                                MinMaxScaler(),
                                                'passthrough']},
                         {'model': [LinearRegression()],
                          'preprocessor__num': [StandardScaler(),
                                                MinMaxScaler(),
                                                'passthrough']},
                         {'model': [DummyRegressor()],
                          'model__strategy': ['mean']}],
             scoring=make_scorer(smape, greater_is_better=False, response_method='predict'))
Pipeline(steps=[('preprocessor',
                 ColumnTransformer(remainder='passthrough',
                                   transformers=[('ohe',
                                                  Pipeline(steps=[('simpleImputer_ohe',
                                                                   SimpleImputer(strategy='most_frequent')),
                                                                  ('ohe',
                                                                   OneHotEncoder(drop='first',
                                                                                 handle_unknown='ignore',
                                                                                 sparse_output=False))]),
                                                  ['dept', 'last_year_promo',
                                                   'last_year_violations']),
                                                 ('ord',
                                                  Pipeline(steps=[('simpleImputer_befor...
                                                                   SimpleImputer(strategy='most_frequent')),
                                                                  ('ord',
                                                                   OrdinalEncoder(categories=[['junior',
                                                                                               'middle',
                                                                                               'sinior'],
                                                                                              ['low',
                                                                                               'medium',
                                                                                               'high']],
                                                                                  handle_unknown='use_encoded_value',
                                                                                  unknown_value=nan)),
                                                                  ('simpleImputer_after_ord',
                                                                   SimpleImputer(strategy='most_frequent'))]),
                                                  ['level', 'workload']),
                                                 ('num', StandardScaler(),
                                                  ['employment_years',
                                                   'supervisor_evaluation',
                                                   'salary'])])),
                ('model', SVR(C=1))])
ColumnTransformer(remainder='passthrough',
                  transformers=[('ohe',
                                 Pipeline(steps=[('simpleImputer_ohe',
                                                  SimpleImputer(strategy='most_frequent')),
                                                 ('ohe',
                                                  OneHotEncoder(drop='first',
                                                                handle_unknown='ignore',
                                                                sparse_output=False))]),
                                 ['dept', 'last_year_promo',
                                  'last_year_violations']),
                                ('ord',
                                 Pipeline(steps=[('simpleImputer_before_ord',
                                                  SimpleImputer(strategy='most_frequent')),
                                                 ('ord',
                                                  OrdinalEncoder(categories=[['junior',
                                                                              'middle',
                                                                              'sinior'],
                                                                             ['low',
                                                                              'medium',
                                                                              'high']],
                                                                 handle_unknown='use_encoded_value',
                                                                 unknown_value=nan)),
                                                 ('simpleImputer_after_ord',
                                                  SimpleImputer(strategy='most_frequent'))]),
                                 ['level', 'workload']),
                                ('num', StandardScaler(),
                                 ['employment_years', 'supervisor_evaluation',
                                  'salary'])])
['dept', 'last_year_promo', 'last_year_violations']
SimpleImputer(strategy='most_frequent')
OneHotEncoder(drop='first', handle_unknown='ignore', sparse_output=False)
['level', 'workload']
SimpleImputer(strategy='most_frequent')
OrdinalEncoder(categories=[['junior', 'middle', 'sinior'],
                           ['low', 'medium', 'high']],
               handle_unknown='use_encoded_value', unknown_value=nan)
SimpleImputer(strategy='most_frequent')
['employment_years', 'supervisor_evaluation', 'salary']
StandardScaler()
[]
passthrough
SVR(C=1)
In [49]:
best_model = grid_search.best_estimator_
print('Лучшая модель и её параметры:\n\n', best_model)
print ('Метрика лучшей модели на тренировочной выборке с использованием кросс-валидации:', grid_search.best_score_*(-1))
Лучшая модель и её параметры:

 Pipeline(steps=[('preprocessor',
                 ColumnTransformer(remainder='passthrough',
                                   transformers=[('ohe',
                                                  Pipeline(steps=[('simpleImputer_ohe',
                                                                   SimpleImputer(strategy='most_frequent')),
                                                                  ('ohe',
                                                                   OneHotEncoder(drop='first',
                                                                                 handle_unknown='ignore',
                                                                                 sparse_output=False))]),
                                                  ['dept', 'last_year_promo',
                                                   'last_year_violations']),
                                                 ('ord',
                                                  Pipeline(steps=[('simpleImputer_befor...
                                                                   SimpleImputer(strategy='most_frequent')),
                                                                  ('ord',
                                                                   OrdinalEncoder(categories=[['junior',
                                                                                               'middle',
                                                                                               'sinior'],
                                                                                              ['low',
                                                                                               'medium',
                                                                                               'high']],
                                                                                  handle_unknown='use_encoded_value',
                                                                                  unknown_value=nan)),
                                                                  ('simpleImputer_after_ord',
                                                                   SimpleImputer(strategy='most_frequent'))]),
                                                  ['level', 'workload']),
                                                 ('num', StandardScaler(),
                                                  ['employment_years',
                                                   'supervisor_evaluation',
                                                   'salary'])])),
                ('model', SVR(C=1))])
Метрика лучшей модели на тренировочной выборке с использованием кросс-валидации: 15.058363611125369
In [50]:
print(f'Метрика SMAPE лучшей модели на тестовой выборке: {round(smape(y_test_1, grid_search.best_estimator_.predict(X_test_1)), 4)}')
Метрика SMAPE лучшей модели на тестовой выборке: 14.014

Вывод:

Лучшей моделью (подходящей условию SMAPE < 15 для тестовой выборки) является - SVR C=1 и ядром rbf, количественные данные которой были закодированы StandardScaler(), а категориальные OneHotEncoder() и OrdinalEncoder();

Значение SMAPE на тренировочной выборке с применением кросс-валидации равно ~ 15.05;
Значение SMAPE на тестовой выборке равно ~ 14.01.

Оформление выводов¶

Определение влияния входных признаков на модель

In [51]:
# Выборка небольшого подмножества данных для ускорения
X_train_sample = shap.sample(X_train_1, 500)
X_test_sample = shap.sample(X_test_1, 500)

# Преобразование данных через preprocessor для ускорения
X_train_transformed = grid_search.best_estimator_['preprocessor'].fit_transform(X_train_sample)

X_test_transformed = grid_search.best_estimator_['preprocessor'].transform(X_test_sample)

# Получаем имена признаков после трансформации
feature_names = grid_search.best_estimator_['preprocessor'].get_feature_names_out()

# Создаем DataFrame для удобства анализа
X_test_enc = pd.DataFrame(X_test_transformed, columns=feature_names)

# Создание SHAP Explainer для модели SVR
explainer = shap.PermutationExplainer(
    model=grid_search.best_estimator_['model'].predict,  # Предсказания через модель
    data=X_train_transformed,  # Преобразованные данные для обучения
    masker=shap.maskers.Independent(data=X_train_transformed)  # Указываем masker
)

# Расчёт SHAP-значений для тестового набора
shap_values = explainer.shap_values(X_test_transformed)

# Преобразуем SHAP-значения в Explanation объект
shap_values = shap.Explanation(
    values=shap_values,         
    feature_names=feature_names,  # Имена признаков
    data=X_test_transformed       # Исходные данные
)
PermutationExplainer explainer: 501it [02:13,  3.53it/s]                                                      
In [52]:
# Визуализация важности признаков с подписями осей
fig, ax = plt.subplots(figsize=(10, 6))  # Создаём фигуру и ось
shap.plots.bar(shap_values, max_display=30, show=False)

# Добавляем подписи осей
ax.set_xlabel("Среднее абсолютное SHAP-значение", fontsize=12)
ax.set_ylabel("Входные признаки", fontsize=12)
ax.set_title("Важность признаков (SHAP)", fontsize=14, fontweight='bold')

plt.show()  # Отображаем график
No description has been provided for this image
In [53]:
shap.plots.beeswarm(shap_values, max_display=30, show=False)

# Добавляем заголовок через `plt`
plt.title("Важность признаков (SHAP)", fontsize=14, fontweight='bold')
plt.xlabel("SHAP-значение (влияние на модель)", fontsize=12)
plt.ylabel("Входные признаки", fontsize=12)

plt.show()  # Отображаем график
No description has been provided for this image

Для поиска лучшей модели был использован Pipeline содержащий следующие шаги:

  • тренировочные и тестовые входные данные были закодированы с помощью: StandardScaler, OneHotEncoder, OrdinalEncoder;
  • в процессе поиска к данным применено 3 типа моделей регрессии: DecisionTreeRegressor, SVR, LinearRegression и DummyRegressor();
  • на основе метрики SMAPE была отобрана лучшая модель - SVR с гиперпараметрами (C=1 и ядром rbf);
  • значение SMAPE на тренировочной выборке с применением кросс-валидации равно ~ 15.51;
  • значение SMAPE на тестовой выборке равно ~ 14.05.

Так же были определены наиболее влияющие входные признаки на целевой job_satisfaction_rate - ими являются: salary, supervisor_evaluation, level и workload.

Модель SVR справилась лучше, чем LinearRegression, т.к. входной признак salary сильно влияет на целевой признак, который связан с целевым нелинейно, это доказывает очень слабая линейная связь на тестовой выборке (коэф. корреляции = 0.17);
Модель DecisionTreeRegressor склонна к переобучению, особенно на малых данных или данных с выбросами (т.к. salary). Это может привести к резким скачкам в предсказаниях.

Задача 2: предсказание увольнения сотрудника из компании¶

Загрузка данных¶

In [54]:
try:
    train_quit = pd.read_csv("C:\\Data-science\\ds_csv\\train_quit.csv")
    test_target_quit = pd.read_csv("C:\\Data-science\\ds_csv\\test_target_quit.csv")
except:
    try:
        train_quit = pd.read_csv('/datasets/train_quit.csv')
        test_target_quit = pd.read_csv('/datasets/test_target_quit.csv')
    except:
        raise FileNotFoundError 

Изучение загруженных датасетов¶

In [55]:
# Создаем словарь, чтобы перебрать все импортируемые датафреймы
dataframes = {
    "train_quit": train_quit,
    "test_target_quit": test_target_quit
}


# Цикл по каждому DataFrame с выводом имени, информации и первых строк
for name, data in dataframes.items():
    print(f'\033[1mНаименование анализируемого датафрейма:\033[0m {name}')
    print()
    data.info()  # Выводим информацию о DataFrame
    display(data.head(5))  # Отображаем первые 5 строк
    print('=' * TERM_SIZE.columns)  # Линия-разделитель по ширине терминала
Наименование анализируемого датафрейма: train_quit

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 4000 entries, 0 to 3999
Data columns (total 10 columns):
 #   Column                 Non-Null Count  Dtype 
---  ------                 --------------  ----- 
 0   id                     4000 non-null   int64 
 1   dept                   4000 non-null   object
 2   level                  4000 non-null   object
 3   workload               4000 non-null   object
 4   employment_years       4000 non-null   int64 
 5   last_year_promo        4000 non-null   object
 6   last_year_violations   4000 non-null   object
 7   supervisor_evaluation  4000 non-null   int64 
 8   salary                 4000 non-null   int64 
 9   quit                   4000 non-null   object
dtypes: int64(4), object(6)
memory usage: 312.6+ KB
id dept level workload employment_years last_year_promo last_year_violations supervisor_evaluation salary quit
0 723290 sales middle high 2 no no 4 54000 no
1 814010 sales junior medium 2 no no 4 27600 no
2 155091 purchasing middle medium 5 no no 1 37200 no
3 257132 sales junior medium 2 no yes 3 24000 yes
4 910140 marketing junior medium 2 no no 5 25200 no
===============================================================================================================
Наименование анализируемого датафрейма: test_target_quit

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 2000 entries, 0 to 1999
Data columns (total 2 columns):
 #   Column  Non-Null Count  Dtype 
---  ------  --------------  ----- 
 0   id      2000 non-null   int64 
 1   quit    2000 non-null   object
dtypes: int64(1), object(1)
memory usage: 31.4+ KB
id quit
0 999029 yes
1 372846 no
2 726767 no
3 490105 no
4 416898 yes
===============================================================================================================

Определение наличия и количества пропусков¶

In [56]:
# Цикл по каждому DataFrame с выводом имени, информации и первых строк
for name, data in dataframes.items():
    print(f'\033[1mНаименование анализируемого датафрейма:\033[0m {name}')
    display(data.isna().sum())  # Отображаем первые 5 строк
    print('=' * TERM_SIZE.columns)  # Линия-разделитель по ширине терминала
Наименование анализируемого датафрейма: train_quit
id                       0
dept                     0
level                    0
workload                 0
employment_years         0
last_year_promo          0
last_year_violations     0
supervisor_evaluation    0
salary                   0
quit                     0
dtype: int64
===============================================================================================================
Наименование анализируемого датафрейма: test_target_quit
id      0
quit    0
dtype: int64
===============================================================================================================

Вывод по предварительному анализу:

В ходе предварительного анализа данных было выявлено:

  • В датафреймах отсутствуют пропуски;
  • Название столбцов имеют форму snake_case.

Предобработка данных¶

Проверка датафреймов на явные дубликаты¶

In [57]:
for name, data in dataframes.items():
    print(f"Явных дубликатов в датасете {name} - {data.duplicated().sum()}: "
          if data.duplicated().sum() > 0 else f"Явных дубликатов в датасете {name} - НЕТ")
Явных дубликатов в датасете train_quit - НЕТ
Явных дубликатов в датасете test_target_quit - НЕТ

Проверка датафреймов на неявные дубликаты¶

In [58]:
for name, data in dataframes.items():
    col_cat = data.select_dtypes(include=['object']).columns.to_list()
    print(f'\033[1mНазвание датафрейма {name}\033[0m')
    for col in col_cat:
        print(f"Уникальные значения в столбце '{col}': {data[col].unique()}")
    print('=' * TERM_SIZE.columns)
Название датафрейма train_quit
Уникальные значения в столбце 'dept': ['sales' 'purchasing' 'marketing' 'technology' 'hr']
Уникальные значения в столбце 'level': ['middle' 'junior' 'sinior']
Уникальные значения в столбце 'workload': ['high' 'medium' 'low']
Уникальные значения в столбце 'last_year_promo': ['no' 'yes']
Уникальные значения в столбце 'last_year_violations': ['no' 'yes']
Уникальные значения в столбце 'quit': ['no' 'yes']
===============================================================================================================
Название датафрейма test_target_quit
Уникальные значения в столбце 'quit': ['yes' 'no']
===============================================================================================================
In [59]:
train_quit['level'] = train_quit['level'].replace('sinior', 'senior')

Вывод по предобработке данных:

В ходе предобработке данных была выполненна проверка датафреймов на дубликаты, в результате которой дубликатов необнаружено, а опечатка в должности senior в датафрейме train_quit исправлена.

Исследовательский анализ данных¶

Объединение тестовой выборки для статистического анализа¶

In [60]:
test_quit_full = test_features.merge(test_target_quit, on='id', how='inner')
display(test_quit_full.shape)
test_quit_full.sample(5)
(2000, 10)
Out[60]:
id dept level workload employment_years last_year_promo last_year_violations supervisor_evaluation salary quit
820 906310 sales senior high 5 no no 5 78000 no
205 508688 marketing junior low 1 no no 3 16800 yes
37 484655 hr junior low 4 no no 4 16800 no
1574 861257 sales junior medium 2 no no 4 25200 no
1114 417411 sales junior low 2 no no 4 12000 yes

Анализ количественных и качественных признаков¶

Для тренировочных данных train_quit¶
In [61]:
# Формирование списка столбцов с количественными признаками
num_variables_col = train_quit.select_dtypes(include=['number']).columns.to_list()
num_variables_col = [col for col in num_variables_col if col not in ['id', 'supervisor_evaluation', 'employment_years']]
num_variables_col
Out[61]:
['salary']
In [62]:
# Вывод графиков для датафрейма train_quit
for col in num_variables_col:
    show_num_variable(train_quit, col, 'quit')
    normal_check(train_quit, col)
    print('=' * TERM_SIZE.columns)
No description has been provided for this image
        Выдвенем гипотезы:
        - Н0: Распределение параметра salary является нормальным.
        - Н1: Распределение параметра salary не является нормальным.
    
Тест Шапиро — Уилка: Stat=0.930, p=1.09e-39
Распределение данных не нормальное с вероятностью более 0.95. Отвергаем нулевую гипотезу
===============================================================================================================
In [63]:
train_quit.groupby('quit')[num_variables_col].describe().round(3).T
Out[63]:
quit no yes
salary count 2872.000 1128.000
mean 37702.228 23885.106
std 15218.977 9351.600
min 12000.000 12000.000
25% 25200.000 16800.000
50% 34800.000 22800.000
75% 46800.000 27600.000
max 96000.000 79200.000

Вывод для количесвенных значений датафрейма train_quit:

  • Распределения количественных значений датафрейма test_quit отличаются от Гауссовсского;
  • Плотность распределения зарплат сотрудников зависит от целевого признака quit;
  • Для уволившихся сотрудников quit = yes заметна более низкая сумма зп, нежели у тех сотрудников кто остался.
  • Среднее значение salary для quit = yes: mean = 23885, медианное значение median = 22800;
  • Среднее значение salary для quit = no: mean = 37702, медианное значение median = 34800.

Есть предположение, что данная зависимость связанна с тем, что чаще увольняются сотрудники с низких должностей, что сильно влияет на анализ. Проверим влияет ли должность сотрудника при увольнении на зарплату.

In [64]:
suptitle = f'Зависимость salary от занимаемо должности для уволившихся сотрудников'

show_num_variable(
    train_quit.query('quit == "yes"'),
    'salary',
    'level',
    suptitle
)
No description has been provided for this image
In [65]:
train_quit.query('quit == "yes"').groupby('level')['salary'].describe().round(3).T
Out[65]:
level junior middle senior
count 1003.000 108.000 17.000
mean 22508.076 33122.222 46447.059
std 7583.086 11971.862 19095.488
min 12000.000 18000.000 25200.000
25% 15600.000 22800.000 31200.000
50% 21600.000 28800.000 45600.000
75% 27600.000 45600.000 60000.000
max 48000.000 62400.000 79200.000
In [66]:
suptitle = f'Зависимость salary от занимаемо должности для оставшихся сотрудников'

show_num_variable(
    train_quit.query('quit == "no"'),
    'salary',
    'level',
    suptitle
)
No description has been provided for this image
In [67]:
train_quit.query('quit == "no"').groupby('level')['salary'].describe().round(3).T
Out[67]:
level junior middle senior
count 946.000 1586.000 340.000
mean 25661.734 40075.914 60130.588
std 5991.478 12101.534 15535.602
min 12000.000 18000.000 25200.000
25% 21600.000 32400.000 50400.000
50% 26400.000 39600.000 58800.000
75% 28800.000 48000.000 73200.000
max 48000.000 70800.000 96000.000

В результате получили:

В среднем у уволившихся сотрудников заработная плата меньше, чем у оставшихся независимо от его должности.

In [68]:
# Формирование списка столбцов с количественными дискретными признаками
discrete_variables_col = ['supervisor_evaluation', 'employment_years']
discrete_variables_col
Out[68]:
['supervisor_evaluation', 'employment_years']
In [69]:
# Вывод графиков для дискретных значений датафрейма 
# train_quit в зависимости от целевого признака quit
for col in discrete_variables_col:
    show_discrete_variable(train_quit, col, 'quit')
    print('=' * TERM_SIZE.columns)
No description has been provided for this image
===============================================================================================================
No description has been provided for this image
===============================================================================================================
In [70]:
train_quit.groupby('quit')[discrete_variables_col].describe().round(3).T
Out[70]:
quit no yes
supervisor_evaluation count 2872.000 1128.000
mean 3.643 3.046
std 0.965 0.973
min 1.000 1.000
25% 3.000 3.000
50% 4.000 3.000
75% 4.000 4.000
max 5.000 5.000
employment_years count 2872.000 1128.000
mean 4.431 1.845
std 2.545 1.275
min 1.000 1.000
25% 2.000 1.000
50% 4.000 1.000
75% 6.000 2.000
max 10.000 10.000

Вывод для дискретных признаков в датафрейма train_quit:

Для уволившихся сотрудников (quit = yes):

  • Для признака supervisor_evaluation (оценка качества работы сотрудника) наиболее популярная оценка 3 (46.4 % от всех уволившихся сотрудников). Среднее значение supervisor_evaluation - mean = 3.046, медианное значение supervisor_evaluation - median = 3;
  • Для признака employment_years (длительности работы в компании) наблюдается, что наибольшее количество уволившихся работников проработали в компании 1 год (53.1 %). Среднее значение employment_years - mean = 1.845, медианное значение employment_years - median = 1.

Для оставшихся сотрудников (quit = no):

  • Для признака supervisor_evaluation (оценка качества работы сотрудника) наиболее популярная оценка 4 (47.6 % от всех оставшихся сотрудников). Среднее значение supervisor_evaluation - mean = 3.643, медианное значение supervisor_evaluation - median = 4;
  • Для признака employment_years (длительности работы в компании) примерно одинаковое соотношение оставшихся работников, есть небольшое преобладание сотрудников имеющих стаж 2 года (17 %) и постепенное уменьшение количества сотрудников начиная с 8 лет и более. Среднее значение employment_years - mean = 4.431, медианное значение employment_years - median = 4.

Наблюдается явная зависимость входных признаков supervisor_evaluation и employment_years от целевого quit.
У уволившихся сотрудников маленький стаж и хуже отношение с руководством.

In [71]:
# Лист с названиями столбцов категориальных признаков
dict_market_file_cat = train_quit.select_dtypes(include=['object']).columns.to_list()

for col in dict_market_file_cat:
    show_cat_variable_by_target(train_quit, col, col)
    print('=' * TERM_SIZE.columns)
No description has been provided for this image
===============================================================================================================
No description has been provided for this image
===============================================================================================================
No description has been provided for this image
===============================================================================================================
No description has been provided for this image
===============================================================================================================
No description has been provided for this image
===============================================================================================================
No description has been provided for this image
===============================================================================================================

Вывод по категориальным данным датафрейма train_quit:

Данные по сотрудникам датафреймов train_job_satisfaction_rate и train_quit схожи Наблюдается дисбаланс целевого признака quit. В компании за 1 год уволилось 1/3 сотрудников (28.2 %)

  • Больше всего сотрудников в компании работает в продажах (36 %), наименьшее число работает в hr;
  • В компании малое количество высококвалифицированных сотрудников (senior = 8.9 %), сотрудников низкой и средней квалификации примерно равное количество (junior = 48.7 %, middle = 42.4 %);
  • Большинство сотрудников в компании имеют среднюю загруженность (их 53 %), наименьшее количество высокозагруженных работников (их 16.9 %);
  • В компании редко происходят повышения, за прошлый год повысили 2.8 % сотрудников;
  • Так же большинство сотрудников соблюдают правила установленные компанией в трудовом договоре (их 86.4 %).
Иследование категориальных данных датафрейма train_quit в зависимости от значения целевого признака quit¶
In [72]:
# Лист с названиями столбцов категориальных признаков
list_cat = train_quit.select_dtypes(include=['object']).columns.to_list()

list_cat.remove('quit')

for col in list_cat:
    show_cat_variable_by_target(train_quit, col, col, 'quit')
    print('=' * TERM_SIZE.columns)
No description has been provided for this image
===============================================================================================================
No description has been provided for this image
===============================================================================================================
No description has been provided for this image
===============================================================================================================
No description has been provided for this image
===============================================================================================================
No description has been provided for this image
===============================================================================================================

Вывод по анализу графиков для датафрейма train_quit

Целевой признак quit (уход из компании) разделен на две категории "no" 71.8 % и "yes" - 28.2 %;
Распределение целевого признак quit имеет дисбаланс;
Целевой признак quit влияет на некоторые категориальные признаки:

  • Входной признак level для целевого признака:
    • quit - no имеет распределение junior - 55.2 %, middle - 32.9 % и senior - 11.8 %;
    • quit - yes имеет распределение junior - 88.9 %, middle - 9.6 % и senior - 1.5 %;
      Это говорит о том, что чаще всего увольняются сотрудники с малым опытом работы;
  • Входной признак workload (уровень загруженности сотрудников) для целевого признака:
    • quit - no имеет распределение low - 56.8 %, medium - 24 % и high - 19.3 %;
    • quit - yes имеет распределение low - 46 %, medium - 43.3 % и high - 10.7 %;
      У сотрудников которые увольняются выше нагруженность;
  • Входной признак last_year_promo (было ли повышение за последний год) для целевого признака:
    • quit - no имеет распределение no - 96.1 %, yes - 3.9 %;
    • quit - yes имеет распределение no - 99.9 %, yes - 0.1 %;
      В компании практически не повышают сотрудников, а сотрудники которые увольняются вообще не повышают;
  • Входной признак last_year_violations (нарушал ли сотрудник трудовой договор за последний год) для целевого признака:
    • quit - no имеет распределение no - 89 %, yes - 11 %;
    • quit - yes имеет распределение no - 79.8 %, yes - 20.2 %;
      В компании чаще нарушают правила сотрудники которые увольняются.

Корреляционный анализ¶

Для корреляционного анализа и впоследствии модели машинного обучения, признак id не нужен, т.к. не несет никакой смысловой нагрузки, а так же он может повлиять на итоговую модель.

In [73]:
# Удаление столбца id
train_quit_without_id = train_quit.drop('id', axis=1)

col_names_corr = ['salary']
big_data_corr = train_quit_without_id.phik_matrix(interval_cols=col_names_corr)

plt.figure(figsize=(14, 10))
sns.heatmap(big_data_corr, annot=True, fmt=".2f", center=0)
plt.title('Матрица корреляций phik')
plt.show()
No description has been provided for this image

Вывод по корреляционному анализу

Мультиколлеанарности между признаками не замечено;

В ходе анализа Phik матрицы корреляций были определены силы связи целевого признака quit со входными, используя шкалу Чеддока:

  • Средняя связь целевого признака прослеживается с входным признаком employment_years (0.66) и salary (0.56):
  • Слабая связь целевого признака прослеживается с входным признаком level (0.31);
  • С остальными входными признаками связь очень слабая.
Для тестовых данных test_quit_full¶
In [74]:
# Формирование списка столбцов с количественными признаками
num_variables_col = test_quit_full.select_dtypes(include=['number']).columns.to_list()
num_variables_col = [col for col in num_variables_col if col not in ['id', 'supervisor_evaluation', 'employment_years']]
num_variables_col
Out[74]:
['salary']
In [75]:
# Вывод графиков для датафрейма test_quit_full
for col in num_variables_col:
    show_num_variable(test_quit_full, col, 'quit')
    normal_check(test_quit_full, col)
    print('=' * TERM_SIZE.columns)
No description has been provided for this image
        Выдвенем гипотезы:
        - Н0: Распределение параметра salary является нормальным.
        - Н1: Распределение параметра salary не является нормальным.
    
Тест Шапиро — Уилка: Stat=0.926, p=2.13e-30
Распределение данных не нормальное с вероятностью более 0.95. Отвергаем нулевую гипотезу
===============================================================================================================
In [76]:
test_quit_full.groupby('quit')[num_variables_col].describe().round(3).T
Out[76]:
quit no yes
salary count 1436.000 564.000
mean 37645.404 24955.319
std 15503.475 10650.301
min 12000.000 12000.000
25% 25200.000 18000.000
50% 33600.000 22800.000
75% 48000.000 30000.000
max 96000.000 80400.000

Аналогично тренировочной выборке проверим зависит ли уход сотрудника от зп на занимаемой должности

In [77]:
suptitle = f'Зависимость salary от занимаемо должности для уволившихся сотрудников'

show_num_variable(
    test_quit_full.query('quit == "yes"'),
    'salary',
    'level',
    suptitle
)
No description has been provided for this image
In [78]:
test_quit_full.query('quit == "yes"').groupby('level')['salary'].describe().round(3).T
Out[78]:
level junior middle senior
count 488.000 62.000 13.000
mean 22790.164 35477.419 57046.154
std 7458.990 13061.788 17442.124
min 12000.000 18000.000 31200.000
25% 15600.000 21900.000 44400.000
50% 21600.000 33600.000 57600.000
75% 27600.000 46800.000 72000.000
max 48000.000 66000.000 80400.000
In [79]:
suptitle = f'Зависимость salary от занимаемо должности для оставшихся сотрудников'

show_num_variable(
    test_quit_full.query('quit == "no"'),
    'salary',
    'level',
    suptitle
)
No description has been provided for this image
In [80]:
test_quit_full.query('quit == "no"').groupby('level')['salary'].describe().round(3).T
Out[80]:
level junior middle senior
count 486.000 792.000 158.000
mean 25866.667 40098.485 61579.747
std 6204.594 12479.692 16030.658
min 12000.000 18000.000 25200.000
25% 21600.000 30000.000 51600.000
50% 26400.000 39600.000 61200.000
75% 30000.000 50400.000 72000.000
max 48000.000 69600.000 96000.000

Аналогично тренировочной выборке, в среднем у уволившихся сотрудников заработная плата меньше, чем у оставшихся независимо от его должности.

In [81]:
# Вывод графиков для дискретных значений датафрейма 
# test_quit_full в зависимости от целевого признака quit
for col in discrete_variables_col:
    show_discrete_variable(test_quit_full, col, 'quit')
    print('=' * TERM_SIZE.columns)
No description has been provided for this image
===============================================================================================================
No description has been provided for this image
===============================================================================================================
In [82]:
test_quit_full.groupby('quit')[discrete_variables_col].describe().round(3).T
Out[82]:
quit no yes
supervisor_evaluation count 1436.000 564.000
mean 3.717 3.043
std 0.959 0.926
min 1.000 1.000
25% 3.000 3.000
50% 4.000 3.000
75% 4.000 4.000
max 5.000 5.000
employment_years count 1436.000 564.000
mean 4.331 1.975
std 2.541 1.553
min 1.000 1.000
25% 2.000 1.000
50% 4.000 1.000
75% 6.000 2.000
max 10.000 10.000
In [83]:
# Лист с названиями столбцов категориальных признаков
dict_market_file_cat = test_quit_full.select_dtypes(include=['object']).columns.to_list()

for col in dict_market_file_cat:
    show_cat_variable_by_target(test_quit_full, col, col)
    print('=' * TERM_SIZE.columns)
No description has been provided for this image
===============================================================================================================
No description has been provided for this image
===============================================================================================================
No description has been provided for this image
===============================================================================================================
No description has been provided for this image
===============================================================================================================
No description has been provided for this image
===============================================================================================================
No description has been provided for this image
===============================================================================================================
Иследование категориальных данных датафрейма test_quit_full в зависимости от значения целевого признака quit¶
In [84]:
# Лист с названиями столбцов категориальных признаков
list_cat = test_quit_full.select_dtypes(include=['object']).columns.to_list()

list_cat.remove('quit')

for col in list_cat:
    show_cat_variable_by_target(test_quit_full, col, col, 'quit')
    print('=' * TERM_SIZE.columns)
No description has been provided for this image
===============================================================================================================
No description has been provided for this image
===============================================================================================================
No description has been provided for this image
===============================================================================================================
No description has been provided for this image
===============================================================================================================
No description has been provided for this image
===============================================================================================================

Корреляционный анализ¶

In [85]:
# Удаление столбца id
test_quit_without_id = test_quit_full.drop('id', axis=1)

col_names_corr = ['salary']
big_data_corr = test_quit_without_id.phik_matrix(interval_cols=col_names_corr)

plt.figure(figsize=(14, 10))
sns.heatmap(big_data_corr, annot=True, fmt=".2f", center=0)
plt.title('Матрица корреляций phik')
plt.show()
No description has been provided for this image

Было замечено отличие в столбце workload для тестовой и тренировочной выборки (в тренировочной у оставшихся сотрудников low - 56.8 %, medium - 24 %, high - 19.3 %, у уволившихся low - 46 %, medium - 43.3 %, high - 10,7 %; в тестовой у оставшихся сотрудников low - 24.8 %, medium - 55.1 %, high - 20.1 %, у уволившихся low - 42 %, medium - 44.7 %, high - 13,3 %).
Данное отличие не существенно, т.к. не имеет сильной корреляции на целевой признак (в тренировочном датафрейме корреляция для workload = 0.13, в тестовой корреляция для workload = 0.1)

Существенных отличий тренировочной и тестовой выборки не выявлено, данные можно использовать для МО

Портрет уходящего работника¶

Сотрудник который увольняется работает недавно, у него зарплата ниже чем у коллег на тех же должностях, его не повышают, он чаще нарушает правила, а начальство хуже его к нему относится.

Визуализация и сравнение распределения признака job_satisfaction_rate для ушедших и оставшихся сотрудников¶

Аналитики утверждают, что уровень удовлетворённости сотрудника работой в компании влияет на то, уволится ли сотрудник. Проверим данное утверждение

In [86]:
test_quit_full = test_quit_full.merge(test_target_job_satisfaction_rate, on='id', how='inner')
display(test_quit_full.shape)
test_quit_full.head(5)
(2000, 11)
Out[86]:
id dept level workload employment_years last_year_promo last_year_violations supervisor_evaluation salary quit job_satisfaction_rate
0 485046 marketing junior medium 2 no no 5 28800 no 0.79
1 686555 hr junior medium 1 no no 4 30000 no 0.72
2 467458 sales middle low 5 no no 4 19200 no 0.64
3 418655 sales middle low 6 no no 4 19200 no 0.60
4 789145 hr middle medium 5 no no 5 40800 no 0.75
In [87]:
show_num_variable(test_quit_full, 'job_satisfaction_rate', 'quit')
No description has been provided for this image
In [88]:
test_quit_full.groupby('quit')['job_satisfaction_rate'].describe().round(3).T
Out[88]:
quit no yes
count 1436.000 564.000
mean 0.612 0.388
std 0.199 0.186
min 0.030 0.040
25% 0.500 0.240
50% 0.660 0.370
75% 0.760 0.492
max 1.000 0.970

Вывод

Увольняющиеся сотрудники имеют более низкий уровень удовлетворённости работой в компании

Общий вывод по исследовательскому анализу

Количественные данные:

  • Распределения количественных значений датафрейма test_quit отличаются от Гауссовсского;
  • Плотность распределения зарплат сотрудников зависит от целевого признака quit;
  • Для уволившихся сотрудников quit = yes заметна более низкая сумма зп, нежели у тех сотрудников кто остался.
  • Среднее значение salary для quit = yes: mean = 23885, медианное значение median = 22800;
  • Среднее значение salary для quit = no: mean = 37702, медианное значение median = 34800.

Дискретные данные: Для уволившихся сотрудников (quit = yes):

  • Для признака supervisor_evaluation (оценка качества работы сотрудника) наиболее популярная оценка 3 (46.4 % от всех уволившихся сотрудников). Среднее значение supervisor_evaluation - mean = 3.046, медианное значение supervisor_evaluation - median = 3;
  • Для признака employment_years (длительности работы в компании) наблюдается, что наибольшее количество уволившихся работников проработали в компании 1 год (53.1 %). Среднее значение employment_years - mean = 1.845, медианное значение employment_years - median = 1.

Для оставшихся сотрудников (quit = no):

  • Для признака supervisor_evaluation (оценка качества работы сотрудника) наиболее популярная оценка 4 (47.6 % от всех оставшихся сотрудников). Среднее значение supervisor_evaluation - mean = 3.643, медианное значение supervisor_evaluation - median = 4;
  • Для признака employment_years (длительности работы в компании) примерно одинаковое соотношение оставшихся работников, есть небольшое преобладание сотрудников имеющих стаж 2 года (17 %) и постепенное уменьшение количества сотрудников начиная с 8 лет и более. Среднее значение employment_years - mean = 4.431, медианное значение employment_years - median = 4.

Наблюдается явная зависимость входных признаков supervisor_evaluation и employment_years от целевого quit.

Категориальные данные:
Данные по сотрудникам датафреймов train_job_satisfaction_rate и train_quit схожи Наблюдается дисбаланс целевого признака quit. В компании за 1 год уволилось 1/3 сотрудников (28.2 %)

Обший анализ категориальных данных:

  • Больше всего сотрудников в компании работает в продажах (36 %), наименьшее число работает в hr;
  • В компании малое количество высококвалифицированных сотрудников (senior = 8.9 %), сотрудников низкой и средней квалификации примерно равное количество (junior = 48.7 %, middle = 42.4 %);
  • Большинство сотрудников в компании имеют среднюю загруженность (их 53 %), наименьшее количество высокозагруженных работников (их 16.9 %);
  • В компании редко происходят повышения, за прошлый год повысили 2.8 % сотрудников;
  • Так же большинство сотрудников соблюдают правила установленные компанией в трудовом договоре (их 86.4 %). Анализ категориальных данных в зависимости от целевого признака quit:
    Целевой признак quit (уход из компании) разделен на две категории "no" 71.8 % и "yes" - 28.2 %;
    Распределение целевого признак quit имеет дисбаланс;
    Целевой признак quit влияет на некоторые категориальные признаки:
  • Входной признак level для целевого признака:
    • quit - no имеет распределение junior - 55.2 %, middle - 32.9 % и senior - 11.8 %;
    • quit - yes имеет распределение junior - 88.9 %, middle - 9.6 % и senior - 1.5 %;
      Это говорит о том, что чаще всего увольняются сотрудники с малым опытом работы;
  • Входной признак workload (уровень загруженности сотрудников) для целевого признака:
    • quit - no имеет распределение low - 56.8 %, medium - 24 % и high - 19.3 %;
    • quit - yes имеет распределение low - 46 %, medium - 43.3 % и high - 10.7 %;
      У сотрудников которые увольняются выше нагруженность;
  • Входной признак last_year_promo (было ли повышение за последний год) для целевого признака:
    • quit - no имеет распределение no - 96.1 %, yes - 3.9 %;
    • quit - yes имеет распределение no - 99.9 %, yes - 0.1 %;
      В компании практически не повышают сотрудников, а сотрудники которые увольняются вообще не повышают;
  • Входной признак last_year_violations (нарушал ли сотрудник трудовой договор за последний год) для целевого признака:
    • quit - no имеет распределение no - 89 %, yes - 11 %;
    • quit - yes имеет распределение no - 79.8 %, yes - 20.2 %;
      В компании чаще нарушают правила сотрудники которые увольняются.

Существенных отличий тренировочной и тестовой выборки не выявлено, данные можно использовать для МО

Добавление нового входного признака¶

In [89]:
train_quit['job_satisfaction_rate'] = best_model.predict(train_quit)
train_quit.sample(5)
Out[89]:
id dept level workload employment_years last_year_promo last_year_violations supervisor_evaluation salary quit job_satisfaction_rate
3039 904468 purchasing junior high 1 no no 5 33600 yes 0.612660
3852 947313 technology junior medium 1 no no 1 30000 yes 0.301611
2495 327728 hr middle medium 9 no yes 4 40800 no 0.636685
1572 151574 sales middle high 5 no no 5 49200 no 0.689072
2023 365585 purchasing middle high 5 no no 3 58800 no 0.600228
In [90]:
data_quit_full = pd.concat([train_quit, test_quit_full])

# Удаление столбца id
data_quit_full_without_id = data_quit_full.drop('id', axis=1)

col_names_corr = data_quit_full_without_id.select_dtypes(include='number').columns.to_list()
big_data_corr = data_quit_full_without_id.phik_matrix(interval_cols=col_names_corr)

plt.figure(figsize=(12, 10))
sns.heatmap(big_data_corr, annot=True, fmt=".2f", center=0)
plt.title('Матрица корреляций phik')
plt.show()
No description has been provided for this image

Вывод по добавлению нового входного признака:

После добавления нового признака мультиколлениарность не появилась, данные пригодны для обучения моделей.

Подготовка данных¶

In [91]:
# Тренировочные данные
X_train_2 = train_quit.drop(['quit', 'id'], axis=1)
y_train_2 = train_quit['quit']
In [92]:
# Тестовые данные
X_test_2 = test_quit_full.drop(['quit', 'id'], axis=1)
y_test_2 = test_quit_full['quit']
In [93]:
# Определение числовых и текстовых признаков
num_columns = X_train_2.select_dtypes(include=['int64', 'float64']).columns.tolist()
ohe_columns = X_train_2.select_dtypes(include=['object']).columns.tolist()
ohe_columns = [col for col in ohe_columns if col not in ['level', 'workload']]
ord_columns = ['level', 'workload']
In [94]:
# Кодировка целевого признака
le = LabelEncoder()
y_train_2 = le.fit_transform(y_train_2)
y_test_2 = le.transform(y_test_2)

Вывод по подготовке данных:

Данные были разделены на тренировочную и тестовую выборку и подготовлены для дальнейшего обучения.

Обучение модели¶

Перечислим особенности данных:

  1. Три признака:
    dept, last_year_promo, last_year_violations — нужно кодировать с помощью OneHotEncoder.
  2. Два признака: level, workload — нужно кодировать с помощью OrdinalEncoder.
  3. Количественных 4 признака:
    employment_years, supervisor_evaluation, salary, job_satisfaction_rate - нужно масштабировать.
  4. В признаках пропуски встречаются и обработаем их в пайплайне.
  5. Целевой признак — quit. Задачу мультиклассовой классификации тут рассматривать не будем.
In [95]:
# создаём пайплайн для подготовки признаков из списка ohe_columns: заполнение пропусков и OHE-кодирование
# SimpleImputer + OHE
ohe_pipe = Pipeline(
    [
        (
            'simpleImputer_ohe', 
            SimpleImputer(missing_values=np.nan, strategy='most_frequent')
        ),
        (
            'ohe', 
            OneHotEncoder(drop='first', handle_unknown='ignore', sparse_output=False)
        )
    ]
)


ord_pipe = Pipeline(
    [
        (
            'simpleImputer_before_ord', 
            SimpleImputer(missing_values=np.nan, strategy='most_frequent')
        ),
        (
            'ord',  
            OrdinalEncoder(
                categories=[
                    ['junior', 'middle', 'sinior'], 
                    ['low', 'medium', 'high']
                ], 
                handle_unknown='use_encoded_value', unknown_value=np.nan
            )
        ),
        (
            'simpleImputer_after_ord', 
            SimpleImputer(missing_values=np.nan, strategy='most_frequent')
        )
    ]
) 


# создаём общий пайплайн для подготовки данных
data_preprocessor = ColumnTransformer(
    [
        ('ohe', ohe_pipe, ohe_columns),
        ('ord', ord_pipe, ord_columns),
        ('num', MinMaxScaler(), num_columns)
    ], 
    remainder='passthrough'
)


pipe_final_2 = Pipeline(
    [
        ('preprocessor', data_preprocessor),
        ('model', DecisionTreeClassifier(random_state=RANDOM_STATE))
    ]
)


param_grid = [
    # словарь для модели KNeighborsClassifier() 
    {
        # название модели
        'model': [KNeighborsClassifier()],
        # указываем гиперпараметр модели n_neighbors
        'model__n_neighbors': range(1, 10),
        # указываем список методов масштабирования
        'preprocessor__num': [StandardScaler(), MinMaxScaler()]   
    },
    # словарь для модели DecisionTreeClassifier()
    {
        'model': [DecisionTreeClassifier(random_state=RANDOM_STATE)],
        'model__max_depth': range(2, 15),
        'model__max_features': range(2, len(num_columns) + len(ohe_columns) + len(ord_columns)),
        'preprocessor__num': [StandardScaler(), MinMaxScaler(), 'passthrough'],
    },
    # словарь для модели SVC()
    {
        'model': [SVC(probability=True, random_state=RANDOM_STATE)],
        'model__C': [0.1, 1, 10],
        'model__gamma': ['scale', 'auto', 0.1, 1],
        'preprocessor__num': [StandardScaler(), MinMaxScaler(), 'passthrough']  

    },
    # словарь для модели LogisticRegression()
    {
        'model': [LogisticRegression(solver='liblinear', penalty='l1', random_state=RANDOM_STATE)],
        'model__C': [0.1, 1, 10],
        'preprocessor__num': [StandardScaler(), MinMaxScaler(), 'passthrough']  
    },
    # словарь для модели DummyClassifier()
    {
        'model': [DummyClassifier(random_state=RANDOM_STATE)],
        'model__strategy': ['most_frequent']
    }
]
In [96]:
grid_search_2 = GridSearchCV(
    pipe_final_2, 
    param_grid=param_grid,
    cv=5,
    scoring='roc_auc',
    n_jobs=-1
)

grid_search_2.fit(X_train_2, y_train_2)
print('Лучшая модель и её параметры:\n\n', grid_search_2.best_estimator_)
print ('Метрика лучшей модели на тренировочной выборке с использованием кросс-валидации:', round(grid_search_2.best_score_, 4))
Лучшая модель и её параметры:

 Pipeline(steps=[('preprocessor',
                 ColumnTransformer(remainder='passthrough',
                                   transformers=[('ohe',
                                                  Pipeline(steps=[('simpleImputer_ohe',
                                                                   SimpleImputer(strategy='most_frequent')),
                                                                  ('ohe',
                                                                   OneHotEncoder(drop='first',
                                                                                 handle_unknown='ignore',
                                                                                 sparse_output=False))]),
                                                  ['dept', 'last_year_promo',
                                                   'last_year_violations']),
                                                 ('ord',
                                                  Pipeline(steps=[('simpleImputer_befor...
                                                                                               'sinior'],
                                                                                              ['low',
                                                                                               'medium',
                                                                                               'high']],
                                                                                  handle_unknown='use_encoded_value',
                                                                                  unknown_value=nan)),
                                                                  ('simpleImputer_after_ord',
                                                                   SimpleImputer(strategy='most_frequent'))]),
                                                  ['level', 'workload']),
                                                 ('num', MinMaxScaler(),
                                                  ['employment_years',
                                                   'supervisor_evaluation',
                                                   'salary',
                                                   'job_satisfaction_rate'])])),
                ('model',
                 SVC(C=10, gamma='auto', probability=True, random_state=42))])
Метрика лучшей модели на тренировочной выборке с использованием кросс-валидации: 0.9327
In [97]:
# Предсказание на тестовой выборке
y_pred_2 = grid_search_2.predict(X_test_2)

# Проверка на наличие метода predict_proba
if hasattr(grid_search_2.best_estimator_['model'], 'predict_proba'):
    # Предсказание вероятностей классов
    proba_new = grid_search_2.predict_proba(X_test_2)[:, 1]

    # Вычисление ROC-AUC на вероятностях
    roc_auc_new = roc_auc_score(y_test_2, proba_new)
    print(f'Метрика ROC-AUC на тестовой выборке: {roc_auc_new:.4f}')
else:
    print("Метод predict_proba не поддерживается для лучшей модели.")
Метрика ROC-AUC на тестовой выборке: 0.9289

Вывод:

Лучшей моделью (подходящей условию ROC-AUC > 0.91 для тестовой выборки) является - SVC(C=10, gamma='auto', probability=True, random_state=42 и ядром rbf), количественные данные которой были закодированы MinMaxScaler(), а категориальные OneHotEncoder() и OrdinalEncoder(). Значение метрики ROC-AUC на тренировочной выборке равно ~ 0.9327;
Значение метрики ROC-AUC на тестовой выборке равно ~ 0.9289.

Оформление выводов¶

In [101]:
X_train_sample = shap.sample(X_train_2, 250)
X_test_sample = shap.sample(X_test_2, 250)

# Преобразуем тренировочные данные
X_train_transformed = grid_search_2.best_estimator_['preprocessor'].fit_transform(X_train_sample)
# Преобразуем тренировочные данные
X_test_transformed = grid_search_2.best_estimator_['preprocessor'].transform(X_test_sample)

# Получаем имена признаков
feature_names = grid_search_2.best_estimator_['preprocessor'].get_feature_names_out()

# Создаем объяснитель SHAP с PermutationExplainer
explainer = shap.PermutationExplainer(
    model=grid_search_2.best_estimator_['model'].predict_proba,  # Предсказания через модель
    data=X_train_transformed,  # Преобразованные данные для обучения
    masker=shap.maskers.Independent(data=X_train_transformed)  # Указываем masker
)

# Преобразуем тестовые данные
X_test_enc = grid_search_2.best_estimator_['preprocessor'].transform(X_test_sample)

# Создаем DataFrame для удобства анализа
X_test_enc = pd.DataFrame(X_test_enc, columns=feature_names)

# Вычисляем SHAP-значения
shap_values = explainer.shap_values(X_test_enc)
PermutationExplainer explainer: 251it [01:58,  1.95it/s]                                                      
In [99]:
shap_values_class_1 = shap.Explanation(
    values=shap_values[:, :, 1],          # SHAP-значения для второго класса
    feature_names=X_test_enc.columns,     # Имена признаков
    data=X_test_enc.values                # Исходные данные
)

# Визуализация важности признаков с подписями осей
fig, ax = plt.subplots(figsize=(10, 6))  # Создаём фигуру и ось
shap.plots.bar(shap_values_class_1, max_display=30, show=False)

# Добавляем подписи осей
ax.set_xlabel("Среднее абсолютное SHAP-значение", fontsize=12)
ax.set_ylabel("Входные признаки", fontsize=12)
ax.set_title("Важность признаков (SHAP)", fontsize=14, fontweight='bold')

plt.show()  # Отображаем график
No description has been provided for this image
In [100]:
shap.plots.beeswarm(shap_values_class_1, max_display=30, show=False)

# Добавляем заголовок через `plt`
plt.title("Важность признаков (SHAP)", fontsize=14, fontweight='bold')
plt.xlabel("SHAP-значение (влияние на модель)", fontsize=12)
plt.ylabel("Входные признаки", fontsize=12)

plt.show()  # Отображаем график
No description has been provided for this image

Для поиска лучшей модели был использован Pipeline содержащий следующие шаги:

  • тренировочные и тестовые входные данные были закодированы с помощью: MinMaxScaler, OneHotEncoder, OrdinalEncoder;
  • в процессе поиска к данным применено 4 типа моделей классификации: DecisionTreeClassifier, SVC, LogisticRegression и KNeighborsClassifier;
  • на основе метрики ROC-AUC была отобрана лучшая модель - SVC(C=10, gamma='auto', probability=True, random_state=42 и ядром rbf), количественные данные которой были закодированы MinMaxScaler(), а категориальные OneHotEncoder() и OrdinalEncoder(). Значение метрики ROC-AUC на тренировочной выборке равно ~ 0.9317;
    Значение метрики ROC-AUC на тестовой выборке равно ~ 0.9272.

Так же были определены наиболее влияющие входные признаки на целевой quit - ими являются: job_satisfaction_rate, level, employment_years, и workload.

Модель SVC справилась лучше, чем LogisticRegression, т.к. один из входных признаков имеющих наибольшее влияние на целевой признак дает 'level', который связан с целевым нелинейно, это доказывает очень слабая линейная связь (коэф. корреляции = 0.31);
Если данные содержат сложные нелинейные связи, SVC может быть более подходящим, чем линейные модели, такие как LogisticRegression, или модели, основанные на расстояниях, как KNeighborsClassifier;
DecisionTreeClassifier может переобучаться, особенно если данные содержат шум или малое количество объектов. Это снижает его способность обобщать данные и уменьшает показатель ROC-AUC. SVC же с правильно настроенной регуляризацией (C) способен игнорировать шум и искать обобщающую границу.

Общий вывод:¶

Для поиска лучшей модели, которая сможет предсказать уровень удовлетворённости сотрудника на основе данных заказчика предприняты шаги:

- провелена загрузка данных;
- проведена предобработка данных;
- проведено исследование полученных данных и признаков;
- была построена модель с метрикой `SMAPE` не более 15 на тестовой выборке;
- в процессе поиска к данным применено 3 типа моделей регрессии;
- на основе метрики `SMAPE` была отобрана лучшая модель `SVR` (C=1 и ядром `rbf`) c использованием пайплайна;  
- метрика `SMAPE` лучшей модели на тренировочной выборке с прнименением кросс-валидации составила: **15.05**;  
- метрика `SMAPE` лучшей модели на тренировочной выборке составила: **14.01**;   

Так же были определены наиболее влияющие входные признаки на целевой job_satisfaction_rate - ими являются: salary, supervisor_evaluation, level и workload.

Для поиска лучшей модели, которая сможет предсказать на основе данных заказчика, что сотрудник уволится из компании предприняты шаги:

- провелена загрузка данных;
- проведена предобработка данных;
- проведено исследование полученных данных и признаков;
- была построена модель с метрикой `ROC-AUC` более 0.91 на тестовой выборке;
- в процессе поиска к данным применено 4 типа моделей классификации;
- на основе метрики `ROC-AUC` была отобрана лучшая модель `SVC` (C=10, gamma='auto', probability=True, random_state=42 и ядром `rbf`) c использованием пайплайна;  
- метрика `ROC-AUC` лучшей модели на тренировочной выборке составила: **0.9327**;  
- метрика `ROC-AUC` лучшей модели на тренировочной выборке составила: **0.9289**;   

Так же были определены наиболее влияющие входные признаки на целевой quit - ими являются: job_satisfaction_rate, level, employment_years, и workload.

Был определен портрет увольняющегося сотрудника:

Сотрудник который увольняется работает недавно, у него зарплата ниже, чем у коллег на тех же должностях, его не повышают, он чаще нарушает правила, а начальство хуже его к нему относится. Так же он не удовлетворен работой (job_satisfaction_rate ниже, чем у других сотрудников).

Рекомендации для бизнеса по снижению уровня увольнения

Признаки удовлетворенность работой и вероятность увольнения очень сильно связаны, поэтому чтобы повысить удовлетворенность работой сотрудника необходимо:

  1. Следить за соответствием заработной платы сотрудника рынку;
  2. Чаще проводить конкурсы для повышения должностей сотрудников (должность напрямую влияет на пункт 1);
  3. Следить чтобы сотрудники были не сильно загружены - это может привести к выгоранию и последующим увольнением сотрудника.